@react-scad/core 0.1.7 → 0.1.9
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 +106 -19
- package/dist/log.d.ts +14 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +25 -0
- package/dist/runtime/root.d.ts.map +1 -1
- package/dist/runtime/root.js +3 -0
- package/dist/runtime/write-on-commit.d.ts.map +1 -1
- package/dist/runtime/write-on-commit.js +11 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,23 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
Render JSX to **OpenSCAD** models using the [React reconciler](https://github.com/facebook/react/tree/main/packages/react-reconciler).
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
5
|
+
- Describe models as a tree of components instead of imperative SCAD; avoids nested modules and parameter threading.
|
|
6
|
+
- Same React/JSX mental model (components, props, composition), output is 3D.
|
|
7
|
+
- Writes plain `.scad` files for [OpenSCAD](https://openscad.org/) or any slicer.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Preview
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
*Rocket example with animated rotation.*
|
|
14
|
+
|
|
15
|
+
## Why react-scad?
|
|
16
|
+
|
|
17
|
+
SCAD is good for parametric 3D but scripts are imperative and nesting gets heavy; composing modules and passing parameters is tedious.
|
|
18
|
+
|
|
19
|
+
A lot of people already think in components and JSX from building UIs. **react-scad** aims to facilitate that same way of thinking for parametric 3D.
|
|
20
|
+
|
|
21
|
+
## Getting Started
|
|
22
|
+
|
|
23
|
+
### Prerequisites
|
|
24
|
+
|
|
25
|
+
- **Node.js** 18+
|
|
26
|
+
- **React** 18 or later (peer dependency)
|
|
27
|
+
|
|
28
|
+
### Install
|
|
12
29
|
|
|
13
30
|
```bash
|
|
14
31
|
npm install react @react-scad/core
|
|
15
32
|
```
|
|
16
33
|
|
|
34
|
+
With pnpm or yarn:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add react @react-scad/core
|
|
38
|
+
# or
|
|
39
|
+
yarn add react @react-scad/core
|
|
40
|
+
```
|
|
41
|
+
|
|
17
42
|
### Minimal example
|
|
18
43
|
|
|
44
|
+
Create a file `main.tsx` (or `main.jsx`):
|
|
45
|
+
|
|
19
46
|
```jsx
|
|
20
47
|
import { createRoot, Cube, Sphere, Union } from "@react-scad/core";
|
|
21
48
|
|
|
49
|
+
// Output path: the .scad file that will be created
|
|
22
50
|
const root = createRoot("model.scad");
|
|
23
51
|
|
|
24
52
|
root.render(
|
|
@@ -29,18 +57,58 @@ root.render(
|
|
|
29
57
|
);
|
|
30
58
|
```
|
|
31
59
|
|
|
32
|
-
|
|
60
|
+
- `createRoot("model.scad")`: creates a root that writes to `model.scad`.
|
|
61
|
+
- `Union`: CSG union of all children (like `union()` in SCAD).
|
|
62
|
+
- `Cube` / `Sphere`: props match SCAD: `size`, `center`, `r`, `$fn`, etc.
|
|
63
|
+
|
|
64
|
+
### Run and write the `.scad` file
|
|
65
|
+
|
|
66
|
+
Run your entry file with [tsx](https://github.com/privatenumber/tsx) so Node can execute the `.tsx`. The `.scad` file is written to the **current working directory** when you call `root.render()`.
|
|
33
67
|
|
|
34
68
|
```bash
|
|
35
|
-
npx
|
|
36
|
-
npx @react-scad/cli run main.tsx --watch
|
|
69
|
+
npx tsx main.tsx
|
|
37
70
|
```
|
|
38
71
|
|
|
39
|
-
|
|
72
|
+
Watch mode (re-run on save):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx tsx watch main.tsx
|
|
76
|
+
```
|
|
40
77
|
|
|
41
|
-
|
|
78
|
+
Run from the directory that contains `main.tsx` (or use the path to it).
|
|
42
79
|
|
|
43
|
-
|
|
80
|
+
### View the result
|
|
81
|
+
|
|
82
|
+
- Open the generated `.scad` file in [OpenSCAD](https://openscad.org/) to preview, export STL, or tweak.
|
|
83
|
+
- Or import the `.scad` (or an exported STL) into your slicer for 3D printing.
|
|
84
|
+
|
|
85
|
+
## Advanced
|
|
86
|
+
|
|
87
|
+
To write to a custom path or get the SCAD string in memory instead of using `createRoot(path)`, use a container and `toScad`:
|
|
88
|
+
|
|
89
|
+
```jsx
|
|
90
|
+
import { createContainer, render, toScad, Cube, Sphere, Union } from "@react-scad/core";
|
|
91
|
+
import { writeFileSync } from "fs";
|
|
92
|
+
|
|
93
|
+
const container = createContainer();
|
|
94
|
+
render(
|
|
95
|
+
<Union>
|
|
96
|
+
<Cube size={[10, 10, 10]} center />
|
|
97
|
+
<Sphere r={6} />
|
|
98
|
+
</Union>,
|
|
99
|
+
container
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const scadCode = toScad(container);
|
|
103
|
+
writeFileSync("out/model.scad", scadCode);
|
|
104
|
+
// or use scadCode however you like
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Primitives (SCAD coverage)
|
|
108
|
+
|
|
109
|
+
All listed SCAD primitives and operations are implemented. Prop names follow SCAD where it makes sense (`r`, `h`, `size`, `center`, `$fn`, etc.).
|
|
110
|
+
|
|
111
|
+
| SCAD | react-scad | Implemented |
|
|
44
112
|
| -------- | ---------- | :---------: |
|
|
45
113
|
| **3D primitives** | | |
|
|
46
114
|
| `cube()` | `Cube` | ✓ |
|
|
@@ -70,22 +138,41 @@ All listed OpenSCAD primitives and operations are implemented. Prop names follow
|
|
|
70
138
|
| `surface()` | `Surface` | ✓ |
|
|
71
139
|
| `import()` | `Import` | ✓ |
|
|
72
140
|
|
|
73
|
-
##
|
|
141
|
+
## Contributing
|
|
142
|
+
|
|
143
|
+
1. **Fork and clone** the repo, then install dependencies:
|
|
144
|
+
```bash
|
|
145
|
+
git clone https://github.com/YOUR_USER/react-scad.git
|
|
146
|
+
cd react-scad
|
|
147
|
+
pnpm install
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
2. **Create a branch** for your change:
|
|
151
|
+
```bash
|
|
152
|
+
git checkout -b fix/your-change
|
|
153
|
+
```
|
|
74
154
|
|
|
75
|
-
|
|
155
|
+
3. **Build and test** before committing:
|
|
156
|
+
```bash
|
|
157
|
+
pnpm run build
|
|
158
|
+
pnpm run dev # optional: smoke-test the example
|
|
159
|
+
```
|
|
76
160
|
|
|
77
|
-
|
|
161
|
+
4. **Format and lint** (all code in the repo):
|
|
162
|
+
```bash
|
|
163
|
+
pnpm run format
|
|
164
|
+
pnpm run lint
|
|
165
|
+
```
|
|
78
166
|
|
|
79
|
-
**
|
|
167
|
+
5. **Open a PR** against `main` with a short description of the change. For bugs, reference the issue if one exists.
|
|
80
168
|
|
|
81
|
-
|
|
169
|
+
6. **Publishing** is done via GitHub Actions on push to `main`; no need to publish from a PR.
|
|
82
170
|
|
|
83
171
|
## Acknowledgements
|
|
84
172
|
|
|
85
173
|
- [React](https://react.dev/) and the [React reconciler](https://github.com/facebook/react/tree/main/packages/react-reconciler) for the rendering model that makes this approach possible.
|
|
86
|
-
- [OpenSCAD](https://openscad.org/) for the
|
|
174
|
+
- [OpenSCAD](https://openscad.org/) for the SCAD language and documentation.
|
|
87
175
|
|
|
88
176
|
## License
|
|
89
177
|
|
|
90
|
-
|
|
91
|
-
contributors.
|
|
178
|
+
MIT
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const c: {
|
|
2
|
+
dim: (s: string) => string;
|
|
3
|
+
cyan: (s: string) => string;
|
|
4
|
+
green: (s: string) => string;
|
|
5
|
+
red: (s: string) => string;
|
|
6
|
+
yellow: (s: string) => string;
|
|
7
|
+
bold: (s: string) => string;
|
|
8
|
+
};
|
|
9
|
+
export declare const log: {
|
|
10
|
+
buildOk(ms: number): void;
|
|
11
|
+
wrote(path: string, ms: number): void;
|
|
12
|
+
watchCycle(ms: number, code: number | null): void;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,CAAC;aACJ,MAAM;cACL,MAAM;eACL,MAAM;aACR,MAAM;gBACH,MAAM;cACR,MAAM;CAChB,CAAC;AAMF,eAAO,MAAM,GAAG;gBACH,MAAM,GAAG,IAAI;gBAGb,MAAM,MAAM,MAAM,GAAG,IAAI;mBAGtB,MAAM,QAAQ,MAAM,GAAG,IAAI,GAAG,IAAI;CAKjD,CAAC"}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const noColor = process.env.NO_COLOR != null || !process.stdout.isTTY;
|
|
2
|
+
export const c = {
|
|
3
|
+
dim: (s) => (noColor ? s : `\x1b[2m${s}\x1b[0m`),
|
|
4
|
+
cyan: (s) => (noColor ? s : `\x1b[36m${s}\x1b[0m`),
|
|
5
|
+
green: (s) => (noColor ? s : `\x1b[32m${s}\x1b[0m`),
|
|
6
|
+
red: (s) => (noColor ? s : `\x1b[31m${s}\x1b[0m`),
|
|
7
|
+
yellow: (s) => (noColor ? s : `\x1b[33m${s}\x1b[0m`),
|
|
8
|
+
bold: (s) => (noColor ? s : `\x1b[1m${s}\x1b[0m`),
|
|
9
|
+
};
|
|
10
|
+
function time() {
|
|
11
|
+
return new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
12
|
+
}
|
|
13
|
+
export const log = {
|
|
14
|
+
buildOk(ms) {
|
|
15
|
+
console.log(c.dim(`[${time()}] `) + c.green("✓ Built") + c.dim(` in ${ms}ms`));
|
|
16
|
+
},
|
|
17
|
+
wrote(path, ms) {
|
|
18
|
+
console.log(c.dim(`[${time()}] `) + c.green("✓ Wrote") + c.dim(` ${path} in ${ms}ms`));
|
|
19
|
+
},
|
|
20
|
+
watchCycle(ms, code) {
|
|
21
|
+
const ok = code === 0;
|
|
22
|
+
const status = ok ? c.green("done") : c.red(`exit ${code}`);
|
|
23
|
+
console.log(` ${ok ? c.green("✓") : c.red("✗")}${c.dim(` ${ms}ms `)}${status}`);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../../src/runtime/root.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../../src/runtime/root.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjD,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,MAAM,WAAW,QAAQ;IACxB,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IAC1C,MAAM,IAAI,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC;AAE1B,wBAAgB,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,QAAQ,CAgBhD;AAED,wBAAgB,eAAe,IAAI,aAAa,CAE/C;AAED,wBAAgB,MAAM,CACrB,OAAO,EAAE,KAAK,CAAC,YAAY,EAC3B,SAAS,EAAE,aAAa,EACxB,QAAQ,CAAC,EAAE,MAAM,IAAI,GACnB,IAAI,CAGN;AAED,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/runtime/root.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from "../log.js";
|
|
1
2
|
import { toScad } from "../serialize/index.js";
|
|
2
3
|
import { createScadContainer } from "./node-ops.js";
|
|
3
4
|
import { createFiberRoot, updateContainer } from "./reconciler.js";
|
|
@@ -9,9 +10,11 @@ export function createRoot(path) {
|
|
|
9
10
|
registerWriteOnCommit(container, path);
|
|
10
11
|
return {
|
|
11
12
|
render(element) {
|
|
13
|
+
const start = Date.now();
|
|
12
14
|
updateContainer(element, fiberRoot, null, null);
|
|
13
15
|
if (path)
|
|
14
16
|
writeAfterCommit(container);
|
|
17
|
+
log.watchCycle(Date.now() - start, 0);
|
|
15
18
|
},
|
|
16
19
|
toScad() {
|
|
17
20
|
return toScad(container);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-on-commit.d.ts","sourceRoot":"","sources":["../../src/runtime/write-on-commit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"write-on-commit.d.ts","sourceRoot":"","sources":["../../src/runtime/write-on-commit.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD,wBAAgB,qBAAqB,CACpC,SAAS,EAAE,aAAa,EACxB,IAAI,EAAE,MAAM,GACV,IAAI,CAEN;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAU/D"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { log } from "../log.js";
|
|
2
4
|
import { toScad } from "../serialize/index.js";
|
|
3
5
|
const writeOnCommitPaths = new WeakMap();
|
|
4
6
|
export function registerWriteOnCommit(container, path) {
|
|
@@ -6,6 +8,13 @@ export function registerWriteOnCommit(container, path) {
|
|
|
6
8
|
}
|
|
7
9
|
export function writeAfterCommit(container) {
|
|
8
10
|
const path = writeOnCommitPaths.get(container);
|
|
9
|
-
if (path)
|
|
11
|
+
if (path) {
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
const dir = dirname(path);
|
|
14
|
+
if (dir !== ".")
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
10
16
|
writeFileSync(path, toScad(container), "utf8");
|
|
17
|
+
const ms = Date.now() - start;
|
|
18
|
+
log.wrote(path, ms);
|
|
19
|
+
}
|
|
11
20
|
}
|