@rulecad/cad 0.1.4 → 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/AGENTS.md +12 -0
- package/README.md +12 -0
- package/bin/cad.mjs +16 -13
- package/lib/assembly.mjs +29 -2
- package/lib/design.mjs +36 -3
- package/lib/versions.mjs +5 -4
- package/package.json +4 -4
package/AGENTS.md
CHANGED
|
@@ -47,6 +47,18 @@ variable says which rule it comes from. Follow these steps; they're what makes a
|
|
|
47
47
|
with ranges from rules. An overlap or gap outside its range is a finding: report it with the numbers, and never
|
|
48
48
|
widen a range to make it pass without saying why.
|
|
49
49
|
- Build bigger things from checked assemblies used as items, rather than placing their objects again.
|
|
50
|
+
- A part is named in fits as `<item>.<part>`; an object that returns one unnamed shape has one part, `body`.
|
|
51
|
+
- A gap fit can't tell whether holes line up. Where a board screws into a part, add a holes fit
|
|
52
|
+
(`{ "holes": "pcb.board", "into": "case.body", "depth": "m2.insert_length" }`).
|
|
53
|
+
- A part built around another (a case around its board) reads it: `"uses": ["node/carrier"]`, then
|
|
54
|
+
`carrier.board_w` and `v.carrier.board_holes`. Never copy another object's numbers by hand.
|
|
55
|
+
|
|
56
|
+
## Circuit boards
|
|
57
|
+
- `"engine": "pcb"` in the .json; `main(v)` returns a board built with `v.pcb` (see @rulecad/pcb's README).
|
|
58
|
+
- Parts come from rule sets where one exists (`v.pcb.xiao`, `qwiic`, `jstph`, `header`), or any standard footprint by
|
|
59
|
+
name (`v.pcb.part('U2', 'soic8', x, y)`). Turn parts in quarter turns with `rotation`, held in a variable.
|
|
60
|
+
- Join pins with `nets` (`GND: ['U1.GND', 'J1.GND']`) and let the autorouter draw them; `cad check` runs the fab's
|
|
61
|
+
design rules. Look at the copper preview: connectors should face the edge their plug comes from.
|
|
50
62
|
|
|
51
63
|
## Talking to the person
|
|
52
64
|
- Give links as one labelled line (`npx cad link <project>/<object> --md`), never a raw URL. If it says
|
package/README.md
CHANGED
|
@@ -92,6 +92,12 @@ which version it holds (`keyboard/keycap@0.1.0`, or `+changes` if it isn't a rel
|
|
|
92
92
|
|
|
93
93
|
- `value`: a number, or arithmetic over numbers and names (`+ - * /`, brackets, `min`, `max`, `round`). Rule sets are
|
|
94
94
|
`<namespace>.<rule>`; the object's own variables are bare names. Parsed, never evaluated as code.
|
|
95
|
+
- **Other objects' numbers:** `"uses": ["node/carrier"]` reads another object as a rule set named after it:
|
|
96
|
+
`carrier.board_w` in an expression, `v.carrier.board_w` in code. It gets the object's variables and, from a board, its
|
|
97
|
+
outline (`board_x`, `board_y`, `board_w`, `board_d`) and, in code, its mounting holes (`v.carrier.board_holes`, each
|
|
98
|
+
`{ x, y, d }`). So a case follows its board: change the board and the case rebuilds around it. `"node/carrier@^0.2.0"`
|
|
99
|
+
also checks the version; `"node/carrier@0.2.0"` reads that release. `cad rules` shows each borrowed number with the
|
|
100
|
+
object and version it came from; two objects can't read each other.
|
|
95
101
|
- `show`: on the rules card `cad show` hands its target (six at most). `interface`: other parts depend on it (versioning will compare these).
|
|
96
102
|
- `tweak: { min, max }`: may be changed with `--set`. Variables without one are fixed by their rule.
|
|
97
103
|
- Inputs that hold names or text, not numbers (the design reads them; expressions can't), all settable with `--set`:
|
|
@@ -196,6 +202,10 @@ other items' variables, so a keycap can sit exactly where its slot meets the swi
|
|
|
196
202
|
- A fit with `*` applies to every copy: `{ "a": "plate", "b": "key-*.switch.bottom", … }`. With `*` on both sides,
|
|
197
203
|
each copy is checked against every other copy near it: `{ "a": "key-*.cap", "b": "key-*.cap", "min": 0.5 }`. Repeated fits are reported
|
|
198
204
|
together: the worst case, and how many of the copies are in range.
|
|
205
|
+
- **Holes fits** follow a board's mounting holes into what they screw into: `{ "holes": "pcb.board", "into":
|
|
206
|
+
"case.body", "depth": "m2.insert_length" }`. Each screw's path must be clear for `depth` mm and solid within `ring`
|
|
207
|
+
mm of it (default: the hole's diameter) half way down. A gap fit can't see this: a board that rests on its bosses
|
|
208
|
+
passes even when the bosses are in the wrong place. `"direction": "up"` for screws from below.
|
|
199
209
|
- `requires` pins versions (`^` same major, at least this version, 0.x included, since a minor release never breaks a
|
|
200
210
|
fit here; `~` same minor); an item can use a release: `"object": "keycap@0.2.1"`, or a variant: `"keycap:oem-r3"`.
|
|
201
211
|
- The check names any `estimate` rule an assembly relies on, through its positions or its objects, so guesses get
|
|
@@ -254,6 +264,8 @@ about 10 s, and a re-check after an edit measures only what the edit moved. The
|
|
|
254
264
|
recently used builds.
|
|
255
265
|
|
|
256
266
|
### Assemblies: values from other parts
|
|
267
|
+
(An object can also read another object directly, outside any assembly: see "Other objects' numbers" under Variables.
|
|
268
|
+
That suits a case built around one board; `from` suits values that only mean something where items meet.)
|
|
257
269
|
An item's `from` sets inputs from other items' values, in the assembly's names: `"from": { "usb_y":
|
|
258
270
|
"board.pcb.pico_y" }`. Every item is built before any is placed, so positions can refer to items listed after them
|
|
259
271
|
(the board placed under the case's top while the case is built from the board's Pico position). `"place": false`
|
package/bin/cad.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fmt, loadRuleSet } from '../lib/rules.mjs';
|
|
|
7
7
|
import { WS, TOOL, paths, config, printerOf } from '../lib/workspace.mjs';
|
|
8
8
|
import { fromKLE } from '@rulecad/keyboard-layout';
|
|
9
9
|
import { target, hasTarget } from '../lib/targets.mjs';
|
|
10
|
-
import { locate, build, pick, lint, readObject, printer, pcbEngine } from '../lib/design.mjs';
|
|
10
|
+
import { locate, build, pick, lint, readObject, printer, pcbEngine, objectSets } from '../lib/design.mjs';
|
|
11
11
|
import { git, tags, bump, bigger, newer, compare, snapshot, materialise, files, prints, printsFile, engineId } from '../lib/versions.mjs';
|
|
12
12
|
import { check } from '@rulecad/printcheck';
|
|
13
13
|
import { preview } from '../lib/preview.mjs';
|
|
@@ -299,7 +299,7 @@ async function assembly(cmd, ref) {
|
|
|
299
299
|
// ---------- links ----------
|
|
300
300
|
// The model as a link: full print precision (triangles), the rules card, and exactly which version it is.
|
|
301
301
|
async function links(d, b, set) {
|
|
302
|
-
const s = d.tag ? { kind: null } : since(d), fil = filaments(d.project);
|
|
302
|
+
const s = d.tag ? { kind: null } : await since(d), fil = filaments(d.project);
|
|
303
303
|
const version = d.version || b.object.version || '0.0.0';
|
|
304
304
|
const src = `${d.project}/${d.name}${d.variant ? `:${d.variant}` : ''}@${version}${s.kind && s.kind !== 'first' ? '+changes' : s.kind === 'first' ? '+unreleased' : ''}`;
|
|
305
305
|
const model = {
|
|
@@ -353,10 +353,12 @@ const KINDS = ['patch', 'minor', 'major'];
|
|
|
353
353
|
const today = () => new Date().toISOString().slice(0, 10);
|
|
354
354
|
|
|
355
355
|
// Unreleased changes: compare with the last release. -> { last, kind, reasons } (kind null: nothing changed)
|
|
356
|
-
|
|
356
|
+
// A design that reads other objects is resolved with their numbers (each side with the objects it named then).
|
|
357
|
+
const snap = async (d) => snapshot(d, (await objectSets(d, readObject(d))).sets);
|
|
358
|
+
async function since(d) {
|
|
357
359
|
const last = tags(d.project, d.name).at(-1);
|
|
358
360
|
if (!last) return { last: null, kind: 'first', reasons: [] };
|
|
359
|
-
return { last, ...compare(
|
|
361
|
+
return { last, ...compare(await snap(materialise(d, last.version)), await snap(d)) };
|
|
360
362
|
}
|
|
361
363
|
|
|
362
364
|
// Only the design's own files (and its project's changelog and print log) may have uncommitted changes.
|
|
@@ -391,7 +393,7 @@ async function releaseObject(d, { batch = false } = {}) {
|
|
|
391
393
|
const raw = lint(fs.readFileSync(d.js, 'utf8')), fails = (b.circuit ? await (await pcbEngine()).checks(b.circuit, b.values, b.matrix) // boards: the board's checks
|
|
392
394
|
: b.object.role === 'reference' ? [] : check(b.triangles, printer(b, d), { parts: b.parts })).filter((r) => r.level === 'fail'); // references aren't printed
|
|
393
395
|
if (raw.length || fails.length) throw new Error(`fix the check first: ${[...raw.map((r) => `raw number ${r.text} on line ${r.line}`), ...fails.map((f) => f.text)].join('; ')}`);
|
|
394
|
-
let { last, kind, reasons } = since(d);
|
|
396
|
+
let { last, kind, reasons } = await since(d);
|
|
395
397
|
if (!kind) throw new Error(`nothing has changed since ${last.version}`);
|
|
396
398
|
const asked = opt('bump');
|
|
397
399
|
if (asked && last) {
|
|
@@ -419,7 +421,7 @@ async function releaseProject(project) {
|
|
|
419
421
|
if (!fs.existsSync(pj)) throw new Error(`no project "${project}"`);
|
|
420
422
|
const p = JSON.parse(fs.readFileSync(pj, 'utf8')), manifest = {};
|
|
421
423
|
for (const name of p.objects) {
|
|
422
|
-
const d = locate(`${project}/${name}`), s = since(d);
|
|
424
|
+
const d = locate(`${project}/${name}`), s = await since(d);
|
|
423
425
|
if (!s.last) throw new Error(`${name} has no release yet: cad release ${project}/${name} -m "…"`);
|
|
424
426
|
if (s.kind) throw new Error(`${name} has changes since ${s.last.version}: release it first`);
|
|
425
427
|
manifest[name] = s.last.version;
|
|
@@ -453,7 +455,8 @@ async function releaseProject(project) {
|
|
|
453
455
|
// Release every object in a project that changed, then the project: the tree may hold changes only to their files.
|
|
454
456
|
async function releaseAll(project) {
|
|
455
457
|
const pj = JSON.parse(fs.readFileSync(path.join(paths.project(project), 'project.json'), 'utf8'));
|
|
456
|
-
const changed =
|
|
458
|
+
const changed = [];
|
|
459
|
+
for (const n of pj.objects) { const d = locate(`${project}/${n}`); if ((await since(d)).kind) changed.push(d); }
|
|
457
460
|
const allowed = new Set([`projects/${project}/project.json`, `projects/${project}/CHANGELOG.md`, `projects/${project}/prints.jsonl`,
|
|
458
461
|
...changed.flatMap((d) => files(d, readObject(d)))]);
|
|
459
462
|
onlyChanged([...allowed]);
|
|
@@ -482,25 +485,25 @@ async function compareVariants(d, a, b) {
|
|
|
482
485
|
console.log(out);
|
|
483
486
|
}
|
|
484
487
|
|
|
485
|
-
function status(project) {
|
|
488
|
+
async function status(project) {
|
|
486
489
|
for (const p of project ? [project] : fs.readdirSync(paths.projects)) {
|
|
487
490
|
const pj = JSON.parse(fs.readFileSync(path.join(paths.project(p), 'project.json'), 'utf8'));
|
|
488
491
|
console.log(`${p}${pj.version ? `@${pj.version}` : ''}`);
|
|
489
492
|
for (const name of pj.objects) {
|
|
490
|
-
const s = since(locate(`${p}/${name}`));
|
|
493
|
+
const s = await since(locate(`${p}/${name}`));
|
|
491
494
|
console.log(` ${name.padEnd(16)} ${s.last ? s.last.version.padEnd(8) : 'unreleased'} ${s.kind === 'first' ? '' : s.kind ? `changed since: needs a ${s.kind} release` : 'up to date'}`);
|
|
492
495
|
}
|
|
493
496
|
}
|
|
494
497
|
}
|
|
495
498
|
|
|
496
|
-
function history(ref) {
|
|
499
|
+
async function history(ref) {
|
|
497
500
|
const [project, name] = ref.split('/');
|
|
498
501
|
if (!name) {
|
|
499
502
|
for (const t of tags(project).reverse()) console.log(`${t.tag} ${t.date} ${t.kind} ${t.message}`);
|
|
500
503
|
return status(project);
|
|
501
504
|
}
|
|
502
505
|
const d = locate(ref), done = prints(d);
|
|
503
|
-
const s = since(d);
|
|
506
|
+
const s = await since(d);
|
|
504
507
|
if (s.kind && s.last) { console.log(`unreleased (needs ${s.kind}):`); for (const r of s.reasons) console.log(` ${r.text}`); }
|
|
505
508
|
for (const t of tags(d.project, d.name).reverse()) {
|
|
506
509
|
console.log(`${t.version.padEnd(8)} ${t.date} ${t.kind.padEnd(5)} ${t.message}`);
|
|
@@ -508,9 +511,9 @@ function history(ref) {
|
|
|
508
511
|
}
|
|
509
512
|
}
|
|
510
513
|
|
|
511
|
-
function diff(d, from, to) {
|
|
514
|
+
async function diff(d, from, to) {
|
|
512
515
|
if (!from) throw new Error('usage: cad diff <project>/<object> <from> [<to>]');
|
|
513
|
-
const a =
|
|
516
|
+
const a = await snap(materialise(d, from)), b = await snap(to ? materialise(d, to) : d);
|
|
514
517
|
const r = compare(a, b);
|
|
515
518
|
console.log(`${d.project}/${d.name} ${from} → ${to || 'now'}: ${r.kind ? `a ${r.kind} change` : 'no changes'}`);
|
|
516
519
|
for (const x of r.reasons) console.log(` ${x.kind.padEnd(5)} ${x.text}`);
|
package/lib/assembly.mjs
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
// Positions are expressions over rules (mx.pitch) and items' variables (cap.lift); rotate is degrees about x, y, z,
|
|
13
13
|
// applied before at (a repeated item turns as a whole, its copies with it). An item can pin a release: "object": "keycap@0.2.1". fits measure the closest gap between two
|
|
14
14
|
// items or item.parts; a negative min allows a press fit to overlap.
|
|
15
|
+
// A holes fit follows each of a board's mounting holes down its screw's path into another part:
|
|
16
|
+
// { "holes": "pcb.board", "into": "case.body", "depth": "m2.insert_length", "label": "Screws find their inserts" }
|
|
17
|
+
// The path must be clear for depth mm (it lands in a hole) and solid within "ring" mm of it (default: the hole's
|
|
18
|
+
// diameter) half way down, so the screw has something to grip. "direction": "up" for screws from below.
|
|
15
19
|
import fs from 'node:fs';
|
|
16
20
|
import crypto from 'node:crypto';
|
|
17
21
|
import path from 'node:path';
|
|
@@ -21,7 +25,7 @@ import { locate, build, readObject, jscad } from './design.mjs';
|
|
|
21
25
|
import { materialise } from './versions.mjs';
|
|
22
26
|
import { satisfies } from '@rulecad/versions';
|
|
23
27
|
import { bounds } from '@rulecad/printcheck';
|
|
24
|
-
import { closest } from '@rulecad/fit';
|
|
28
|
+
import { closest, inside } from '@rulecad/fit';
|
|
25
29
|
import { keys as keyCentres, readLayout, hasLayout } from './layout.mjs';
|
|
26
30
|
|
|
27
31
|
export const assemblyPath = (ref) => { const [project, name] = String(ref).split('/'); return path.join(paths.project(project || ''), 'assemblies', `${name || ''}.json`); };
|
|
@@ -181,7 +185,8 @@ export async function buildAssembly(ref, stack = [], overrides = {}, opts = {})
|
|
|
181
185
|
}
|
|
182
186
|
// A fit with * applies to every copy: "key-*.switch.bottom" becomes one fit for key-0-0, key-0-1, …
|
|
183
187
|
// With * on both sides, each copy is checked against all the others: { "a": "key-*.cap", "b": "key-*.cap" }
|
|
184
|
-
for (
|
|
188
|
+
for (let x of a.fits || []) {
|
|
189
|
+
if (x.holes) x = { ...x, a: x.holes, b: x.into }; // a holes fit: a board's holes (a) over what its screws go into (b)
|
|
185
190
|
const star = [x.a, x.b].find((n) => n.includes('*'));
|
|
186
191
|
if (!star) { fits.push({ ...x, num }); continue; }
|
|
187
192
|
const match = (n) => { const re = new RegExp(`^${n.split('.')[0].replace(/[-]/g, '\\-').replace('*', '([\\w-]+)')}$`); return ids.filter((id) => re.test(id)); };
|
|
@@ -258,6 +263,7 @@ export function checkAssembly(s) {
|
|
|
258
263
|
const pick = (n) => (Array.isArray(n) ? n.flatMap(pick) : s.parts.filter((p) => named(p, n)));
|
|
259
264
|
const groups = new Map();
|
|
260
265
|
for (const x of s.fits) {
|
|
266
|
+
if (x.holes) { holesFit(x); continue; }
|
|
261
267
|
const A = pick(x.a), key = x.note || `${x.a} / ${x.b}`;
|
|
262
268
|
const lo = x.min != null ? x.num(x.min) : -Infinity, hi = x.max != null ? x.num(x.max) : Infinity;
|
|
263
269
|
const reach = Math.max(2, ...[lo, hi].filter(Number.isFinite)) + 1; // look past the range, so a gap near it is measured
|
|
@@ -281,6 +287,27 @@ export function checkAssembly(s) {
|
|
|
281
287
|
f(worst.ok ? 'pass' : 'warn', `${key}: ${say(worst.gap)}${n}${worst.ok ? '' : `, wanted ${lo === -Infinity ? '' : `${mm(lo)} `}to ${hi === Infinity ? 'any' : mm(hi)} mm (${results.filter((r) => !r.ok).map((r) => r.id).join(', ')})`}`,
|
|
282
288
|
`${label}: ${worst.gap < 0 ? `press fit ${mm(-worst.gap)} mm³` : worst.gap === Infinity ? 'apart' : `gap ${Number(worst.gap.toFixed(2))} mm`}${worst.ok ? '' : ' (out of range)'}`);
|
|
283
289
|
}
|
|
290
|
+
// Holes fits: each mounting hole's screw path, clear into a hole, with material round it
|
|
291
|
+
function holesFit(x) {
|
|
292
|
+
const A = pick(x.a), B = pick(x.b), label = x.label || `${x.a} into ${x.b}`;
|
|
293
|
+
const board = A.length && s.objects.find((o) => o.id === A[0].leaf)?.built;
|
|
294
|
+
if (!board?.circuit) return f('fail', `${label}: ${x.a} isn't a circuit board, so it has no mounting holes to follow`);
|
|
295
|
+
if (!B.length) return f('fail', `${label}: no such ${x.b}`);
|
|
296
|
+
const holes = board.circuit.filter((e) => e.type === 'pcb_hole' && !e.pcb_component_id);
|
|
297
|
+
if (!holes.length) return f('fail', `${label}: ${x.a} has no mounting holes`);
|
|
298
|
+
const depth = x.num(x.depth), down = x.direction === 'up' ? 1 : -1, faces = B.flatMap((p) => p.triangles);
|
|
299
|
+
const base = down < 0 ? 0 : board.values.thickness ?? 0, at = (h, dx, dy, t) => jscad.maths.vec3.transform([0, 0, 0], [h.x + dx, h.y + dy, base + down * t], A[0].m);
|
|
300
|
+
const steps = 8, misses = [];
|
|
301
|
+
for (const h of holes) {
|
|
302
|
+
const ring = x.ring != null ? x.num(x.ring) : h.hole_diameter, where = `hole at ${mm(h.x)}, ${mm(h.y)} mm`;
|
|
303
|
+
const blocked = Array.from({ length: steps }, (_, i) => (depth * (i + 0.5)) / steps).find((t) => inside(at(h, 0, 0, t), faces));
|
|
304
|
+
if (blocked != null) { misses.push(`${where}: solid ${mm(blocked)} mm along the screw's path`); continue; }
|
|
305
|
+
const round = Array.from({ length: steps }, (_, i) => (2 * Math.PI * i) / steps).filter((a) => !inside(at(h, ring * Math.cos(a), ring * Math.sin(a), depth / 2), faces));
|
|
306
|
+
if (round.length) misses.push(`${where}: no material ${mm(ring)} mm from the screw on ${round.length} of ${steps} sides`);
|
|
307
|
+
}
|
|
308
|
+
f(misses.length ? 'fail' : 'pass', misses.length ? `${label}: ${misses.join('; ')}` : `${label}: ${holes.length} of ${holes.length} holes clear for ${mm(depth)} mm, with material around each`,
|
|
309
|
+
`${label}: ${misses.length ? `${misses.length} of ${holes.length} miss` : `${holes.length} of ${holes.length} line up`}`);
|
|
310
|
+
}
|
|
284
311
|
// Plates print everything: no references
|
|
285
312
|
if (s.a.kind === 'plate') {
|
|
286
313
|
const refs = s.objects.filter((o) => o.role === 'reference' && !o.unplaced); // an unplaced item only lends its numbers
|
package/lib/design.mjs
CHANGED
|
@@ -23,7 +23,8 @@ import { resolve, loadRuleSet } from './rules.mjs';
|
|
|
23
23
|
import { WS, paths, printerOf } from './workspace.mjs';
|
|
24
24
|
import { readLayout, hasLayout, keys, width, depth } from './layout.mjs';
|
|
25
25
|
import * as shapes from '@rulecad/shapes';
|
|
26
|
-
import { engineId, packageVersion } from './versions.mjs';
|
|
26
|
+
import { engineId, packageVersion, materialise } from './versions.mjs';
|
|
27
|
+
import { satisfies } from '@rulecad/versions';
|
|
27
28
|
|
|
28
29
|
const require = createRequire(import.meta.url);
|
|
29
30
|
const jscad = require('@jscad/modeling');
|
|
@@ -44,6 +45,36 @@ export function locate(ref) {
|
|
|
44
45
|
return { project: path.basename(dir), name: path.basename(base), js: `${base}.js`, json: `${base}.json`, dir, ...(variant ? { variant } : {}) };
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// ---------- objects that read each other ----------
|
|
49
|
+
// "uses": ["node/carrier"] reads another object's numbers as a rule set named after it (carrier.board_w in an
|
|
50
|
+
// expression, v.carrier.board_w in code): its variables, and what a board tells the parts around it (board_x, board_y,
|
|
51
|
+
// board_w, board_d, and board_holes for code). The object is built (cached) first. "node/carrier@^0.2.0" also checks
|
|
52
|
+
// its version; "node/carrier@0.2.0" reads that release instead of the working copy.
|
|
53
|
+
export const isObjectRef = (u) => String(u).includes('/');
|
|
54
|
+
const building = new Set();
|
|
55
|
+
export async function objectSets(d, object) {
|
|
56
|
+
const refs = (object.uses || []).filter(isObjectRef), sets = {}, extra = {};
|
|
57
|
+
if (!refs.length) return { sets, extra };
|
|
58
|
+
const self = `${d.project}/${d.name}`;
|
|
59
|
+
building.add(self);
|
|
60
|
+
try {
|
|
61
|
+
for (const u of refs) {
|
|
62
|
+
const [ref, want] = u.split('@'), exact = /^\d+\.\d+\.\d+$/.test(want || '');
|
|
63
|
+
if (building.has(ref)) throw new Error(`${self} and ${ref} read each other's numbers: one of them has to give its numbers to the other, not both`);
|
|
64
|
+
const base = locate(ref), b = await build(exact ? materialise(base, want) : base), version = b.object.version;
|
|
65
|
+
if (want && !exact && !satisfies(version, want)) throw new Error(`${self} uses ${ref} ${want}, but ${ref} is ${version}: change the range, or the design to suit ${version}`);
|
|
66
|
+
const units = Object.fromEntries(Object.entries(b.object.variables || {}).map(([k, x]) => [k, x.unit]));
|
|
67
|
+
const rules = {};
|
|
68
|
+
for (const [k, v] of Object.entries(b.values)) if (typeof v === 'number' && Number.isFinite(v)) {
|
|
69
|
+
rules[k] = { value: v, unit: units[k] ?? 'mm', rule: `${ref}'s ${k}`, kind: 'design', source: `${ref}@${version}` };
|
|
70
|
+
}
|
|
71
|
+
sets[u] = { name: base.name, rules };
|
|
72
|
+
if (b.circuit) extra[base.name] = { board_holes: (await pcbEngine()).holes(b.circuit) };
|
|
73
|
+
}
|
|
74
|
+
} finally { building.delete(self); }
|
|
75
|
+
return { sets, extra };
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
// The layouts an object can pick, by name, from its layout variables' choices (built-ins like grid have no file)
|
|
48
79
|
export function layoutsOf(d, object) {
|
|
49
80
|
const out = {};
|
|
@@ -85,15 +116,17 @@ export const readObject = (d) => {
|
|
|
85
116
|
export async function build(d, set = {}, { mirror = false } = {}) {
|
|
86
117
|
const object = readObject(d);
|
|
87
118
|
set = variantSet(d, object, set);
|
|
88
|
-
const
|
|
119
|
+
const refs = await objectSets(d, object); // other objects' numbers, when it reads them
|
|
120
|
+
const sets = { ...(d.rulesDir ? Object.fromEntries((object.uses || []).filter((u) => !isObjectRef(u)).map((u) => [u, loadRuleSet(u, d.rulesDir)])) : {}), ...refs.sets }; // a release's own rules
|
|
89
121
|
const display = mirror ? object.mirror || {} : {};
|
|
90
122
|
const { values, entries } = resolve(object, { set, sets, display }); // mirror: lighter for the display
|
|
123
|
+
for (const [n, x] of Object.entries(refs.extra)) Object.assign(values[n], x);
|
|
91
124
|
const files = layoutsOf(d, object);
|
|
92
125
|
if (object.layouts) values.layouts = object.layouts; // older designs kept layouts in their .json
|
|
93
126
|
if (Object.keys(files).length) values.layouts = Object.fromEntries(Object.entries(files).map(([n, l]) => [n, { ...l, keys: keys(l), width: width(l), depth: depth(l) }]));
|
|
94
127
|
|
|
95
128
|
// The cache: everything the build is made from. A hit skips running the design (and its booleans) altogether
|
|
96
|
-
const ruleText = (object.uses || []).map((u) => JSON.stringify(sets[u] || loadRuleSet(u)));
|
|
129
|
+
const ruleText = [...(object.uses || []).map((u) => JSON.stringify(sets[u] || loadRuleSet(u))), JSON.stringify(refs.extra)];
|
|
97
130
|
const key = crypto.createHash('sha256').update(JSON.stringify([CACHE_VERSION, own, object.engine === 'pcb' ? engineId() : '', fs.readFileSync(d.js, 'utf8'), JSON.stringify(object), ruleText, files, set, display])).digest('hex');
|
|
98
131
|
const hit = path.join(CACHE, `${key}.json`);
|
|
99
132
|
if (!process.env.CAD_NO_CACHE && fs.existsSync(hit)) {
|
package/lib/versions.mjs
CHANGED
|
@@ -39,7 +39,7 @@ const layoutNames = (d, object) => Object.values(object.variables || {}).filter(
|
|
|
39
39
|
// The files a design depends on, relative to the workspace (its git repo): its .js and .json, the rule sets and the layouts it uses.
|
|
40
40
|
// (A board's engine is the tool's code, not the workspace's: its release records engineId() instead.)
|
|
41
41
|
export function files(d, object) {
|
|
42
|
-
return [path.relative(WS, d.js), path.relative(WS, d.json), ...(object.uses || []).map((u) => `rules/${u}.json`),
|
|
42
|
+
return [path.relative(WS, d.js), path.relative(WS, d.json), ...(object.uses || []).filter((u) => !u.includes('/')).map((u) => `rules/${u}.json`),
|
|
43
43
|
...layoutNames(d, object).map((n) => `projects/${d.project}/layouts/${n}.json`)];
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -54,7 +54,7 @@ export function materialise(d, version) {
|
|
|
54
54
|
const js = path.join(base, `${d.name}.js`), json = path.join(base, `${d.name}.json`);
|
|
55
55
|
fs.writeFileSync(js, show(path.relative(WS, d.js)));
|
|
56
56
|
fs.writeFileSync(json, JSON.stringify(object, null, 2));
|
|
57
|
-
for (const u of object.uses || []) fs.writeFileSync(path.join(base, 'rules', `${u}.json`), show(`rules/${u}.json`));
|
|
57
|
+
for (const u of (object.uses || []).filter((x) => !x.includes('/'))) fs.writeFileSync(path.join(base, 'rules', `${u}.json`), show(`rules/${u}.json`)); // objects it reads are released on their own
|
|
58
58
|
// The layouts it could pick, as they were then (only files that existed in that release)
|
|
59
59
|
fs.mkdirSync(path.join(base, 'layouts'), { recursive: true });
|
|
60
60
|
for (const v of Object.values(object.variables || {})) if (v.type === 'layout') for (const n of v.choice || [v.value]) {
|
|
@@ -91,9 +91,10 @@ function taggedEngine(tag) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// Resolve a design as it is on disk, or as materialised from a release.
|
|
94
|
-
|
|
94
|
+
// refs: the numbers of objects it reads (design.mjs objectSets), which a snapshot can't build itself.
|
|
95
|
+
export function snapshot(d, refs = {}) {
|
|
95
96
|
const object = JSON.parse(fs.readFileSync(d.json, 'utf8'));
|
|
96
|
-
const sets = d.rulesDir ? Object.fromEntries((object.uses || []).map((u) => [u, loadRuleSet(u, d.rulesDir)])) : {};
|
|
97
|
+
const sets = { ...(d.rulesDir ? Object.fromEntries((object.uses || []).filter((u) => !u.includes('/')).map((u) => [u, loadRuleSet(u, d.rulesDir)])) : {}), ...refs };
|
|
97
98
|
const layouts = Object.fromEntries(layoutNames(d, object).map((n) => [n, readLayout(d.project, n, d.layoutsDir)]));
|
|
98
99
|
// A board is also made by the board engine (footprints, the router): which engine, as recorded at the release
|
|
99
100
|
const engine = object.engine === 'pcb' ? (d.tag ? taggedEngine(d.tag) : engineId()) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulecad/cad",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The cad command: JSCAD designs from traceable rules, checked for printing and fit, versioned by what fits, shared as links and exported as Bambu 3MF.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,16 +46,16 @@
|
|
|
46
46
|
"@rulecad/variables": "^0.1.0",
|
|
47
47
|
"@rulecad/keyboard-layout": "^0.1.2",
|
|
48
48
|
"@rulecad/printcheck": "^0.1.0",
|
|
49
|
-
"@rulecad/fit": "^0.
|
|
49
|
+
"@rulecad/fit": "^0.2.0",
|
|
50
50
|
"@rulecad/model-link": "^0.1.2",
|
|
51
51
|
"@rulecad/versions": "^0.1.0",
|
|
52
52
|
"@rulecad/shapes": "^0.1.0",
|
|
53
|
-
"@rulecad/rules": "^0.
|
|
53
|
+
"@rulecad/rules": "^0.2.0",
|
|
54
54
|
"@jscad/modeling": "^2.12.0",
|
|
55
55
|
"three": "^0.186.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@rulecad/pcb": "^0.1.0"
|
|
58
|
+
"@rulecad/pcb": "^0.1.0 || ^0.2.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependenciesMeta": {
|
|
61
61
|
"@rulecad/pcb": {
|