@mylasertools/kerfscript 0.2.0 → 0.2.1

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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /*! @mylasertools/kerfscript | (c) MyLaserTools | AGPL-3.0-only or commercial licence, see COMMERCIAL-LICENSE.md. Includes Clipper2 (BSL-1.0) and opentype.js (MIT), see THIRD-PARTY-NOTICES.md. */
2
+ /*! @mylasertools/kerfscript | (c) MyLaserTools | AGPL-3.0-only or commercial licence, see COMMERCIAL-LICENSE.md. Includes Clipper2 (BSL-1.0), opentype.js, tiny-inflate and string.prototype.codepointat (MIT), see THIRD-PARTY-NOTICES.md. */
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -32,9 +32,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  mod
33
33
  ));
34
34
 
35
- // node_modules/opentype.js/dist/opentype.js
35
+ // ../kerfscript/node_modules/opentype.js/dist/opentype.js
36
36
  var require_opentype = __commonJS({
37
- "node_modules/opentype.js/dist/opentype.js"(exports, module) {
37
+ "../kerfscript/node_modules/opentype.js/dist/opentype.js"(exports, module) {
38
38
  (function(global, factory) {
39
39
  typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = global || self, factory(global.opentype = {}));
40
40
  })(exports, (function(exports2) {
@@ -12400,8 +12400,8 @@ var FUNCTIONS = {
12400
12400
  atan: math1("atan", (x) => Math.atan(x) / RAD),
12401
12401
  atan2: (a) => clean(Math.atan2(num(a[0], "atan2"), num(a[1], "atan2")) / RAD),
12402
12402
  pow: (a) => Math.pow(num(a[0], "pow"), num(a[1], "pow")),
12403
- min: (a) => minmax(a, (xs) => Math.min(...xs), "min"),
12404
- max: (a) => minmax(a, (xs) => Math.max(...xs), "max"),
12403
+ min: (a) => minmax(a, (xs) => xs.reduce((m, x) => x < m ? x : m, Infinity), "min"),
12404
+ max: (a) => minmax(a, (xs) => xs.reduce((m, x) => x > m ? x : m, -Infinity), "max"),
12405
12405
  clamp: (a) => Math.min(Math.max(num(a[0], "clamp"), num(a[1], "clamp")), num(a[2], "clamp")),
12406
12406
  lerp: (a) => {
12407
12407
  const t = num(a[2], "lerp");
@@ -12411,7 +12411,7 @@ var FUNCTIONS = {
12411
12411
  }
12412
12412
  return num(a[0], "lerp") + (num(a[1], "lerp") - num(a[0], "lerp")) * t;
12413
12413
  },
12414
- norm: (a) => Math.hypot(...list(a[0], "norm").map((x) => num(x, "norm"))),
12414
+ norm: (a) => Math.sqrt(list(a[0], "norm").reduce((s, x) => s + num(x, "norm") ** 2, 0)),
12415
12415
  cross: (a) => {
12416
12416
  const u = list(a[0], "cross").map((x) => num(x, "cross"));
12417
12417
  const v = list(a[1], "cross").map((x) => num(x, "cross"));
@@ -12474,7 +12474,7 @@ var FUNCTIONS = {
12474
12474
  type_of: (a) => typeName(a[0])
12475
12475
  };
12476
12476
 
12477
- // node_modules/clipper2-ts/dist/Core.js
12477
+ // ../kerfscript/node_modules/clipper2-ts/dist/Core.js
12478
12478
  var ClipType;
12479
12479
  (function(ClipType2) {
12480
12480
  ClipType2[ClipType2["NoClip"] = 0] = "NoClip";
@@ -13133,7 +13133,7 @@ var RectDUtils = {
13133
13133
  var InvalidRect64 = Object.freeze(Rect64Utils.createInvalid());
13134
13134
  var InvalidRectD = Object.freeze(RectDUtils.createInvalid());
13135
13135
 
13136
- // node_modules/clipper2-ts/dist/Engine.js
13136
+ // ../kerfscript/node_modules/clipper2-ts/dist/Engine.js
13137
13137
  var B02 = BigInt(0);
13138
13138
  var B22 = BigInt(2);
13139
13139
  var B42 = BigInt(4);
@@ -15898,7 +15898,7 @@ var Clipper = {
15898
15898
  }
15899
15899
  };
15900
15900
 
15901
- // node_modules/clipper2-ts/dist/Offset.js
15901
+ // ../kerfscript/node_modules/clipper2-ts/dist/Offset.js
15902
15902
  var JoinType;
15903
15903
  (function(JoinType2) {
15904
15904
  JoinType2[JoinType2["Miter"] = 0] = "Miter";
@@ -16511,7 +16511,7 @@ var ClipperOffset = class _ClipperOffset {
16511
16511
  }
16512
16512
  };
16513
16513
 
16514
- // node_modules/clipper2-ts/dist/Clipper.js
16514
+ // ../kerfscript/node_modules/clipper2-ts/dist/Clipper.js
16515
16515
  var B23 = BigInt(2);
16516
16516
  function union(subject, clipOrFillRule, fillRule) {
16517
16517
  if (typeof clipOrFillRule === "number") {
@@ -16672,21 +16672,31 @@ function merge(shapes) {
16672
16672
  for (const s of shapes) {
16673
16673
  for (const [k, l] of s.layers) {
16674
16674
  const t = out.layers.get(k) ?? { regions: [], lines: [] };
16675
- t.regions.push(...l.regions);
16676
- t.lines.push(...l.lines);
16675
+ for (const p of l.regions) t.regions.push(p);
16676
+ for (const p of l.lines) t.lines.push(p);
16677
16677
  out.layers.set(k, t);
16678
16678
  }
16679
- out.parts.push(...s.parts);
16679
+ for (const p of s.parts) out.parts.push(p);
16680
16680
  }
16681
16681
  return out;
16682
16682
  }
16683
- function retag(s, layer) {
16683
+ function allOn(layers, layer) {
16684
16684
  const all = { regions: [], lines: [] };
16685
- for (const l of s.layers.values()) {
16686
- all.regions.push(...l.regions);
16687
- all.lines.push(...l.lines);
16685
+ for (const l of layers.values()) {
16686
+ for (const p of l.regions) all.regions.push(p);
16687
+ for (const p of l.lines) all.lines.push(p);
16688
16688
  }
16689
- return { layers: /* @__PURE__ */ new Map([[layer, all]]), parts: s.parts };
16689
+ return /* @__PURE__ */ new Map([[layer, all]]);
16690
+ }
16691
+ function retag(s, layer) {
16692
+ if (s.parts.length && layer !== "guide")
16693
+ throw new Error(
16694
+ `a part cannot go on the ${layer} layer as a whole. Put ${layer}() inside part() instead.`
16695
+ );
16696
+ return {
16697
+ layers: allOn(s.layers, layer),
16698
+ parts: s.parts.map((p) => ({ ...p, layers: allOn(p.layers, layer) }))
16699
+ };
16690
16700
  }
16691
16701
  function bounds(polys) {
16692
16702
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
@@ -16703,7 +16713,7 @@ function layerBounds(layers, skip) {
16703
16713
  const all = [];
16704
16714
  for (const [k, l] of layers) {
16705
16715
  if (skip?.(k)) continue;
16706
- all.push(...l.regions, ...l.lines);
16716
+ for (const p of [...l.regions, ...l.lines]) all.push(p);
16707
16717
  }
16708
16718
  return bounds(all);
16709
16719
  }
@@ -17413,13 +17423,14 @@ function flatten(cmds, tolerance = 0.02) {
17413
17423
  } else {
17414
17424
  finish(true);
17415
17425
  last = start;
17416
- cur = [];
17426
+ cur = [start];
17417
17427
  }
17418
17428
  }
17419
17429
  finish(false);
17420
17430
  return { closed, open };
17421
17431
  }
17422
17432
  function parseSvgPath(d2) {
17433
+ const isCommand = (t) => /^[a-zA-Z]$/.test(t);
17423
17434
  const tokens = d2.match(/[a-zA-Z]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g) ?? [];
17424
17435
  const out = [];
17425
17436
  let i = 0;
@@ -17429,8 +17440,7 @@ function parseSvgPath(d2) {
17429
17440
  let prev = "";
17430
17441
  const num2 = () => {
17431
17442
  const t = tokens[i++];
17432
- if (t === void 0 || /[a-zA-Z]/.test(t))
17433
- throw new Error(`Bad path data near "${t ?? "end"}"`);
17443
+ if (t === void 0 || isCommand(t)) throw new Error(`Bad path data near "${t ?? "end"}"`);
17434
17444
  return Number(t);
17435
17445
  };
17436
17446
  const flag = () => {
@@ -17444,8 +17454,10 @@ function parseSvgPath(d2) {
17444
17454
  return Number(t);
17445
17455
  };
17446
17456
  while (i < tokens.length) {
17447
- if (/[a-zA-Z]/.test(tokens[i])) cmd = tokens[i++];
17457
+ if (isCommand(tokens[i])) cmd = tokens[i++];
17448
17458
  else if (!cmd) throw new Error("Path data must start with a command");
17459
+ else if (cmd === "Z" || cmd === "z")
17460
+ throw new Error(`Bad path data near "${tokens[i]}": Z takes no numbers`);
17449
17461
  const rel = cmd === cmd.toLowerCase();
17450
17462
  const C = cmd.toUpperCase();
17451
17463
  const ox = rel ? x : 0, oy = rel ? y : 0;
@@ -17657,6 +17669,31 @@ var DEFAULT_LAYERS = [
17657
17669
  { name: "cut", mode: "cut", color: "#ff0000" },
17658
17670
  { name: "guide", mode: "guide", color: "#00a0ff" }
17659
17671
  ];
17672
+ var DEFAULT_LIMITS = {
17673
+ maxParts: 1e4,
17674
+ maxSegments: 1e4,
17675
+ maxListLength: 1e6,
17676
+ maxListItems: 3e6,
17677
+ maxTextLength: 2e3,
17678
+ maxPoints: 2e6,
17679
+ maxOutputPoints: 1e6,
17680
+ maxSheets: 200
17681
+ };
17682
+ var SHAPE_COST = 100;
17683
+ var LIST_COST = 4;
17684
+ var CHARS_PER_ITEM = 8;
17685
+ var shapePoints = (s) => {
17686
+ let n2 = SHAPE_COST;
17687
+ const add = (layers) => {
17688
+ for (const l of layers.values()) {
17689
+ for (const p of l.regions) n2 += p.length;
17690
+ for (const p of l.lines) n2 += p.length;
17691
+ }
17692
+ };
17693
+ add(s.layers);
17694
+ for (const p of s.parts) add(p.layers);
17695
+ return n2;
17696
+ };
17660
17697
  var Env = class {
17661
17698
  constructor(parent, caller) {
17662
17699
  this.parent = parent;
@@ -17696,11 +17733,15 @@ var Evaluator = class {
17696
17733
  this.opts = opts;
17697
17734
  this.steps = 0;
17698
17735
  this.depth = 0;
17736
+ this.pointCount = 0;
17737
+ this.itemCount = 0;
17738
+ this.copies = 0;
17699
17739
  this.echoes = [];
17700
17740
  this.warnings = [];
17701
17741
  this.layers = new Map(DEFAULT_LAYERS.map((l) => [l.name, l]));
17702
17742
  this.loaded = /* @__PURE__ */ new Map();
17703
17743
  this.maxSteps = opts.maxSteps ?? 5e6;
17744
+ this.limits = { ...DEFAULT_LIMITS, ...opts.limits };
17704
17745
  this.builtins = this.makeBuiltins();
17705
17746
  }
17706
17747
  run(program) {
@@ -17747,16 +17788,28 @@ var Evaluator = class {
17747
17788
  if (s.kind === "module") env.mods.set(s.name, { params: s.params, body: s.body, env });
17748
17789
  else if (s.kind === "function") env.fns.set(s.name, { params: s.params, body: s.body, env });
17749
17790
  else if (s.kind === "use" && !s.include) {
17750
- const lib = this.load(s.path, s.pos);
17791
+ const body = this.included(this.load(s.path, s.pos).body, /* @__PURE__ */ new Set([s.path]));
17751
17792
  const libEnv = new Env(env);
17752
- this.hoist(lib.body, libEnv);
17753
- for (const ls of lib.body)
17793
+ this.hoist(body, libEnv);
17794
+ for (const ls of body)
17754
17795
  if (ls.kind === "assign") this.assign(ls.name, this.expr(ls.value, libEnv), libEnv);
17755
17796
  for (const [k, v] of libEnv.mods) env.mods.set(k, v);
17756
17797
  for (const [k, v] of libEnv.fns) env.fns.set(k, v);
17757
17798
  }
17758
17799
  }
17759
17800
  }
17801
+ // A library's statements with the files it includes written out in place
17802
+ included(stmts, seen) {
17803
+ const out = [];
17804
+ for (const s of stmts) {
17805
+ if (s.kind !== "use" || !s.include) out.push(s);
17806
+ else if (!seen.has(s.path)) {
17807
+ seen.add(s.path);
17808
+ for (const t of this.included(this.load(s.path, s.pos).body, seen)) out.push(t);
17809
+ }
17810
+ }
17811
+ return out;
17812
+ }
17760
17813
  assign(name, value, env) {
17761
17814
  env.vars.set(name, value);
17762
17815
  }
@@ -17855,8 +17908,10 @@ var Evaluator = class {
17855
17908
  });
17856
17909
  for (const k of args.named.keys()) {
17857
17910
  if (k.startsWith("$")) env.vars.set(k, args.named.get(k));
17858
- else if (!names.has(k))
17859
- this.warnings.push(`${what}: unknown argument "${k}" (line ${args.at.line})`);
17911
+ else if (!names.has(k)) {
17912
+ const warning = `${what}: unknown argument "${k}" (line ${args.at.line})`;
17913
+ if (!this.warnings.includes(warning)) this.warnings.push(warning);
17914
+ }
17860
17915
  }
17861
17916
  void defEnv;
17862
17917
  }
@@ -17892,20 +17947,28 @@ var Evaluator = class {
17892
17947
  childList: () => {
17893
17948
  const e = childEnv();
17894
17949
  this.hoist(s.children, e);
17895
- const out = [];
17950
+ const out2 = [];
17896
17951
  for (const st of s.children) {
17897
17952
  const r = this.stmt(st, e);
17898
- if (kids.includes(st)) out.push(r ?? emptyShape());
17953
+ if (kids.includes(st)) out2.push(r ?? emptyShape());
17899
17954
  }
17900
- return out;
17955
+ return out2;
17901
17956
  }
17902
17957
  };
17958
+ let out;
17903
17959
  try {
17904
- return b(ctx);
17960
+ out = b(ctx);
17905
17961
  } catch (e) {
17906
17962
  if (e instanceof KerfError) throw e;
17907
17963
  throw new KerfError(`${s.name}(): ${e.message}`, s.pos);
17908
17964
  }
17965
+ this.pointCount += shapePoints(out);
17966
+ if (this.pointCount > this.limits.maxPoints)
17967
+ throw new KerfError(
17968
+ `The shapes have more than ${this.limits.maxPoints} points, the limit. Use fewer or simpler shapes, or a lower $fn.`,
17969
+ s.pos
17970
+ );
17971
+ return out;
17909
17972
  }
17910
17973
  childStmts(stmts) {
17911
17974
  return stmts.filter(
@@ -17943,7 +18006,8 @@ var Evaluator = class {
17943
18006
  }
17944
18007
  case "list": {
17945
18008
  const out = [];
17946
- for (const it of x.items) this.listItem(it, env, out);
18009
+ for (const it of x.items) this.listItem(it, env, out, x.pos);
18010
+ this.charge(LIST_COST, x.pos);
17947
18011
  return out;
17948
18012
  }
17949
18013
  case "range": {
@@ -18005,30 +18069,50 @@ var Evaluator = class {
18005
18069
  }
18006
18070
  }
18007
18071
  }
18008
- listItem(it, env, out) {
18072
+ listItem(it, env, out, at) {
18009
18073
  switch (it.kind) {
18010
- case "item":
18011
- out.push(this.expr(it.expr, env));
18074
+ case "item": {
18075
+ const v = this.expr(it.expr, env);
18076
+ this.grow(out, 1, at);
18077
+ out.push(v);
18012
18078
  return;
18079
+ }
18013
18080
  case "each": {
18014
- const v = this.expr(it.expr, env);
18015
- out.push(...this.iterable(v, { line: 0, col: 0, offset: 0 }));
18081
+ const xs = this.iterable(this.expr(it.expr, env), at);
18082
+ this.grow(out, xs.length, at);
18083
+ for (const x of xs) out.push(x);
18016
18084
  return;
18017
18085
  }
18018
18086
  case "for":
18019
- this.loop(it.assigns, env, (e) => this.listItem(it.body, e, out));
18087
+ this.loop(it.assigns, env, (e) => this.listItem(it.body, e, out, at));
18020
18088
  return;
18021
18089
  case "if":
18022
- if (truthy(this.expr(it.cond, env))) this.listItem(it.then, env, out);
18023
- else if (it.else) this.listItem(it.else, env, out);
18090
+ if (truthy(this.expr(it.cond, env))) this.listItem(it.then, env, out, at);
18091
+ else if (it.else) this.listItem(it.else, env, out, at);
18024
18092
  return;
18025
18093
  case "let": {
18026
18094
  const e = new Env(env);
18027
18095
  this.bindSeq(it.assigns, e);
18028
- this.listItem(it.body, e, out);
18096
+ this.listItem(it.body, e, out, at);
18029
18097
  }
18030
18098
  }
18031
18099
  }
18100
+ grow(out, n2, at) {
18101
+ if (out.length + n2 > this.limits.maxListLength)
18102
+ throw new KerfError(
18103
+ `a list of ${out.length + n2} items is more than the limit of ${this.limits.maxListLength}`,
18104
+ at
18105
+ );
18106
+ this.charge(n2, at);
18107
+ }
18108
+ charge(items, at) {
18109
+ this.itemCount += items;
18110
+ if (!(this.itemCount <= this.limits.maxListItems))
18111
+ throw new KerfError(
18112
+ `The lists and strings hold more than ${this.limits.maxListItems} items in all, the limit. Is a loop building more than it needs?`,
18113
+ at
18114
+ );
18115
+ }
18032
18116
  call(x, env) {
18033
18117
  if (x.callee.kind === "var") {
18034
18118
  const name = x.callee.name;
@@ -18041,11 +18125,17 @@ var Evaluator = class {
18041
18125
  const b = FUNCTIONS[name];
18042
18126
  if (b) {
18043
18127
  const args = this.evalArgs(x.args, env, x.pos);
18128
+ let out;
18044
18129
  try {
18045
- return b(args.pos, args.named);
18130
+ out = b(args.pos, args.named);
18046
18131
  } catch (e) {
18047
- throw new KerfError(e.message, x.pos);
18132
+ throw new KerfError(
18133
+ e instanceof RangeError ? `${name}(): the result is too large` : e.message,
18134
+ x.pos
18135
+ );
18048
18136
  }
18137
+ this.checkValue(out, x.pos, name);
18138
+ return out;
18049
18139
  }
18050
18140
  throw new KerfError(`Unknown function ${name}()`, x.pos);
18051
18141
  }
@@ -18060,7 +18150,7 @@ var Evaluator = class {
18060
18150
  specialFunction(name, x, env) {
18061
18151
  if (name === "echo") {
18062
18152
  const a = this.evalArgs(x.args, env, x.pos);
18063
- this.echoes.push(this.echoText(a));
18153
+ this.echo(this.echoText(a), x.pos);
18064
18154
  return { handled: true, value: void 0 };
18065
18155
  }
18066
18156
  if (name === "assert") {
@@ -18088,6 +18178,10 @@ var Evaluator = class {
18088
18178
  }
18089
18179
  return { handled: false };
18090
18180
  }
18181
+ echo(text, at) {
18182
+ this.charge(Math.ceil(text.length / CHARS_PER_ITEM), at);
18183
+ this.echoes.push(text);
18184
+ }
18091
18185
  echoText(a) {
18092
18186
  const parts = a.pos.map((v) => typeof v === "string" ? v : show(v));
18093
18187
  for (const [k, v] of a.named) parts.push(`${k} = ${show(v)}`);
@@ -18110,7 +18204,10 @@ var Evaluator = class {
18110
18204
  return Math.pow(a, b);
18111
18205
  }
18112
18206
  }
18113
- if (op === "+" && typeof a === "string" && typeof b === "string") return a + b;
18207
+ if (op === "+" && typeof a === "string" && typeof b === "string") {
18208
+ this.checkSize(a.length + b.length, true, at);
18209
+ return a + b;
18210
+ }
18114
18211
  if (Array.isArray(a) && Array.isArray(b)) {
18115
18212
  if (op === "+" || op === "-") {
18116
18213
  if (a.length !== b.length)
@@ -18118,12 +18215,14 @@ var Evaluator = class {
18118
18215
  `Can't ${op === "+" ? "add" : "subtract"} lists of length ${a.length} and ${b.length}`,
18119
18216
  at
18120
18217
  );
18218
+ this.charge(a.length + LIST_COST, at);
18121
18219
  return a.map((x, i) => this.arith(op, x, b[i], at));
18122
18220
  }
18123
18221
  if (op === "*") {
18124
18222
  if (Array.isArray(a[0])) return a.map((row) => this.arith("*", row, b, at));
18125
18223
  if (Array.isArray(b[0])) {
18126
18224
  const cols = b[0].length;
18225
+ this.checkSize(cols, false, at);
18127
18226
  return Array.from(
18128
18227
  { length: cols },
18129
18228
  (_, j) => a.reduce(
@@ -18137,10 +18236,14 @@ var Evaluator = class {
18137
18236
  return a.reduce((s, x, i) => s + this.arith("*", x, b[i], at), 0);
18138
18237
  }
18139
18238
  }
18140
- if (Array.isArray(a) && typeof b === "number" && (op === "*" || op === "/"))
18239
+ if (Array.isArray(a) && typeof b === "number" && (op === "*" || op === "/")) {
18240
+ this.charge(a.length + LIST_COST, at);
18141
18241
  return a.map((x) => this.arith(op, x, b, at));
18142
- if (typeof a === "number" && Array.isArray(b) && op === "*")
18242
+ }
18243
+ if (typeof a === "number" && Array.isArray(b) && op === "*") {
18244
+ this.charge(b.length + LIST_COST, at);
18143
18245
  return b.map((x) => this.arith(op, a, x, at));
18246
+ }
18144
18247
  const verb = {
18145
18248
  "+": "add",
18146
18249
  "-": "subtract",
@@ -18157,20 +18260,21 @@ var Evaluator = class {
18157
18260
  return def;
18158
18261
  }
18159
18262
  num(v, fn, name) {
18160
- if (typeof v !== "number" || Number.isNaN(v))
18161
- throw new Error(`${name} must be a number, got ${show(v)}`);
18263
+ if (typeof v !== "number" || !Number.isFinite(v))
18264
+ throw new Error(`${name} must be a finite number, got ${show(v)}`);
18162
18265
  return v;
18163
18266
  }
18164
18267
  vec2(v, name, scalarBoth = true) {
18165
- if (typeof v === "number" && scalarBoth) return [v, v];
18166
- if (Array.isArray(v) && typeof v[0] === "number")
18167
- return [v[0], typeof v[1] === "number" ? v[1] : 0];
18268
+ const ok = (x) => typeof x === "number" && Number.isFinite(x);
18269
+ if (ok(v) && scalarBoth) return [v, v];
18270
+ if (Array.isArray(v) && ok(v[0]) && (v[1] === void 0 || ok(v[1])))
18271
+ return [v[0], v[1] ?? 0];
18168
18272
  throw new Error(`${name} must be a number or [x, y], got ${show(v)}`);
18169
18273
  }
18170
18274
  points(v, name) {
18171
18275
  if (!Array.isArray(v)) throw new Error(`${name} must be a list of [x, y] points`);
18172
18276
  return v.map((p, i) => {
18173
- if (!Array.isArray(p) || typeof p[0] !== "number" || typeof p[1] !== "number")
18277
+ if (!Array.isArray(p) || !Number.isFinite(p[0]) || !Number.isFinite(p[1]))
18174
18278
  throw new Error(`${name}[${i}] must be an [x, y] point, got ${show(p)}`);
18175
18279
  return [p[0], p[1]];
18176
18280
  });
@@ -18178,11 +18282,38 @@ var Evaluator = class {
18178
18282
  dyn(env, name) {
18179
18283
  return env.get(name).value;
18180
18284
  }
18285
+ spend(points) {
18286
+ this.pointCount += points;
18287
+ if (!(this.pointCount <= this.limits.maxPoints))
18288
+ throw new Error(
18289
+ `the shapes need more than ${this.limits.maxPoints} points, the limit. Use fewer or simpler shapes.`
18290
+ );
18291
+ }
18292
+ checkValue(v, pos, fn) {
18293
+ if (Array.isArray(v) || typeof v === "string")
18294
+ this.checkSize(v.length, typeof v === "string", pos, fn);
18295
+ }
18296
+ checkSize(n2, isString, pos, fn) {
18297
+ if (n2 > this.limits.maxListLength)
18298
+ throw new KerfError(
18299
+ `${fn ? `${fn}(): ` : ""}${isString ? `a string of ${n2} characters` : `a list of ${n2} items`} is more than the limit of ${this.limits.maxListLength}`,
18300
+ pos
18301
+ );
18302
+ this.charge(isString ? Math.ceil(n2 / CHARS_PER_ITEM) : n2 + LIST_COST, pos);
18303
+ }
18181
18304
  segs(env) {
18182
18305
  const fn = Number(this.dyn(env, "$fn")) || 0;
18183
18306
  const fa = Number(this.dyn(env, "$fa")) || 2;
18184
18307
  const fs = Number(this.dyn(env, "$fs")) || 0.2;
18185
- return (r) => fragments(r, fn, fa, fs);
18308
+ const max = this.limits.maxSegments;
18309
+ return (r) => {
18310
+ const n2 = fragments(r, fn, fa, fs);
18311
+ if (!(n2 <= max))
18312
+ throw new Error(
18313
+ `a circle with ${n2} segments is more than the limit of ${max}. Lower $fn, or raise $fa or $fs.`
18314
+ );
18315
+ return n2;
18316
+ };
18186
18317
  }
18187
18318
  thickness(env) {
18188
18319
  const t = this.dyn(env, "$thickness");
@@ -18253,12 +18384,16 @@ var Evaluator = class {
18253
18384
  Math.floor(this.num(this.arg(c.args, "n", 0, 6), "regular_polygon", "n"))
18254
18385
  );
18255
18386
  const r = this.num(this.arg(c.args, "r", 1, 10), "regular_polygon", "r");
18387
+ if (!(n2 <= this.limits.maxSegments))
18388
+ throw new Error(`${n2} corners is more than the limit of ${this.limits.maxSegments}`);
18256
18389
  return regionShape([ellipsePoly(r, r, n2)]);
18257
18390
  },
18258
18391
  star: (c) => {
18259
18392
  const n2 = Math.max(2, Math.floor(this.num(this.arg(c.args, "n", 0, 5), "star", "n")));
18260
18393
  const r = this.num(this.arg(c.args, "r", 1, 10), "star", "r");
18261
18394
  const ri = this.num(this.arg(c.args, "inner", 2, r * 0.45), "star", "inner");
18395
+ if (!(n2 * 2 <= this.limits.maxSegments))
18396
+ throw new Error(`${n2} points is more than the limit of ${this.limits.maxSegments / 2}`);
18262
18397
  const p = [];
18263
18398
  for (let i = 0; i < n2 * 2; i++) {
18264
18399
  const a = Math.PI / 2 + Math.PI * i / n2;
@@ -18298,6 +18433,10 @@ var Evaluator = class {
18298
18433
  const a0 = this.num(this.arg(c.args, "start", 1, 0), "arc", "start");
18299
18434
  const a1 = this.num(this.arg(c.args, "end", 2, 90), "arc", "end");
18300
18435
  const n2 = Math.max(2, Math.ceil(this.segs(c.env)(r) * Math.abs(a1 - a0) / 360));
18436
+ if (!(n2 <= this.limits.maxSegments))
18437
+ throw new Error(
18438
+ `an arc with ${n2} segments is more than the limit of ${this.limits.maxSegments}`
18439
+ );
18301
18440
  const p = [];
18302
18441
  for (let i = 0; i <= n2; i++) {
18303
18442
  const a = (a0 + (a1 - a0) * i / n2) * Math.PI / 180;
@@ -18311,13 +18450,12 @@ var Evaluator = class {
18311
18450
  "text() needs a font. Pass fonts to render(), or --font on the command line."
18312
18451
  );
18313
18452
  const text = this.arg(c.args, "text", 0, "");
18314
- return regionShape(
18315
- textRegions(
18316
- this.opts.fonts,
18317
- typeof text === "string" ? text : show(text),
18318
- this.textOpts(c.args, c.env)
18319
- )
18320
- );
18453
+ const shown = typeof text === "string" ? text : show(text);
18454
+ if (shown.length > this.limits.maxTextLength)
18455
+ throw new Error(
18456
+ `${shown.length} characters is more than the limit of ${this.limits.maxTextLength} for one text()`
18457
+ );
18458
+ return regionShape(textRegions(this.opts.fonts, shown, this.textOpts(c.args, c.env)));
18321
18459
  },
18322
18460
  translate: (c) => {
18323
18461
  const [x, y] = this.vec2(this.arg(c.args, "v", 0, [0, 0]), "v", false);
@@ -18386,7 +18524,7 @@ var Evaluator = class {
18386
18524
  key = k;
18387
18525
  first = false;
18388
18526
  }
18389
- for (const p of [...l.regions, ...l.lines]) pts.push(...p);
18527
+ for (const p of [...l.regions, ...l.lines]) for (const q of p) pts.push(q);
18390
18528
  }
18391
18529
  const h = hull(pts);
18392
18530
  return {
@@ -18439,6 +18577,9 @@ var Evaluator = class {
18439
18577
  part: (c) => {
18440
18578
  const name = String(this.arg(c.args, "name", 0, "part"));
18441
18579
  const qty = Math.max(1, Math.floor(Number(this.arg(c.args, "qty", 1, 1)) || 1));
18580
+ this.copies += qty;
18581
+ if (!(this.copies <= this.limits.maxParts))
18582
+ throw new Error(`more than ${this.limits.maxParts} part copies in total, the limit`);
18442
18583
  const s = c.children();
18443
18584
  return {
18444
18585
  layers: /* @__PURE__ */ new Map(),
@@ -18487,7 +18628,7 @@ var Evaluator = class {
18487
18628
  return merge(out);
18488
18629
  },
18489
18630
  echo: (c) => {
18490
- this.echoes.push(this.echoText(c.args));
18631
+ this.echo(this.echoText(c.args), c.args.at);
18491
18632
  return c.children();
18492
18633
  },
18493
18634
  assert: (c) => {
@@ -18503,15 +18644,17 @@ var Evaluator = class {
18503
18644
  const [w, h] = this.vec2(this.arg(c.args, "size", 0), "size");
18504
18645
  const edges = this.edges(this.arg(c.args, "edges", 1, "out"));
18505
18646
  const t = this.thickness(c.env);
18506
- return regionShape(fingerRect(w, h, edges, t, this.fingerWidth(c.args, c.env, 2)));
18647
+ const finger = this.fingerWidth(c.args, c.env, 2);
18648
+ this.spend(2 * (Math.abs(w) + Math.abs(h)) / finger * 4);
18649
+ return regionShape(fingerRect(w, h, edges, t, finger));
18507
18650
  },
18508
18651
  finger_holes: (c) => {
18509
18652
  const L = this.num(this.arg(c.args, "length", 0), "finger_holes", "length");
18510
18653
  const e = this.edges(this.arg(c.args, "edge", 1, "out"))[0];
18511
18654
  if (e === "flat") throw new Error('edge must be "out" or "in"');
18512
- return regionShape(
18513
- fingerHoles(L, e, this.thickness(c.env), this.fingerWidth(c.args, c.env, 2))
18514
- );
18655
+ const finger = this.fingerWidth(c.args, c.env, 2);
18656
+ this.spend(Math.abs(L) / finger * 4);
18657
+ return regionShape(fingerHoles(L, e, this.thickness(c.env), finger));
18515
18658
  },
18516
18659
  tslot: (c) => {
18517
18660
  const d2 = this.num(this.arg(c.args, "d", 0, 3), "tslot", "d");
@@ -18531,6 +18674,7 @@ var Evaluator = class {
18531
18674
  );
18532
18675
  if (spacing <= 0 || bridge <= 0 || cut <= 0)
18533
18676
  throw new Error("spacing, bridge and cut must be positive");
18677
+ this.spend((Math.abs(w) / spacing + 1) * (Math.abs(h) / (cut + bridge) + 2) * 2);
18534
18678
  return lineShape(livingHinge(w, h, spacing, bridge, cut));
18535
18679
  }
18536
18680
  };
@@ -18538,14 +18682,20 @@ var Evaluator = class {
18538
18682
  const out = { regions: [], lines: [] };
18539
18683
  for (const s of shapes) {
18540
18684
  if (s.parts.length) throw new Error(`part() can only be the first child of ${fn}()`);
18541
- for (const l of s.layers.values()) out.regions.push(...l.regions);
18685
+ for (const l of s.layers.values()) for (const p of l.regions) out.regions.push(p);
18542
18686
  }
18543
18687
  return out;
18544
18688
  }
18545
18689
  function mapLayers(s, f) {
18546
- const layers = /* @__PURE__ */ new Map();
18547
- for (const [k, l] of s.layers) layers.set(k, f(l));
18548
- return { layers, parts: s.parts };
18690
+ const each = (from) => {
18691
+ const layers = /* @__PURE__ */ new Map();
18692
+ for (const [k, l] of from) layers.set(k, f(l));
18693
+ return layers;
18694
+ };
18695
+ return {
18696
+ layers: each(s.layers),
18697
+ parts: s.parts.map((p) => ({ ...p, layers: each(p.layers) }))
18698
+ };
18549
18699
  }
18550
18700
  }
18551
18701
  };
@@ -18556,7 +18706,18 @@ var n = (v) => {
18556
18706
  return Object.is(r, -0) ? "0" : String(r);
18557
18707
  };
18558
18708
  var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
18559
- var d = (p, closed) => `M${p.map(([x, y]) => `${n(x)} ${n(y)}`).join("L")}${closed ? "Z" : ""}`;
18709
+ function tidy(p, closed) {
18710
+ const out = [];
18711
+ for (const [x, y] of p) {
18712
+ const q = [n(x), n(y)];
18713
+ const prev = out[out.length - 1];
18714
+ if (!prev || prev[0] !== q[0] || prev[1] !== q[1]) out.push(q);
18715
+ }
18716
+ while (closed && out.length > 1 && out[0][0] === out[out.length - 1][0] && out[0][1] === out[out.length - 1][1])
18717
+ out.pop();
18718
+ return out;
18719
+ }
18720
+ var d = (p, closed) => `M${tidy(p, closed).map(([x, y]) => `${x} ${y}`).join("L")}${closed ? "Z" : ""}`;
18560
18721
  function layerPaths(items, layers) {
18561
18722
  const out = [];
18562
18723
  for (const def of layers) {
@@ -18673,11 +18834,14 @@ function toDxf(items, _width, height, layers) {
18673
18834
  g(20, 0);
18674
18835
  g(30, 0);
18675
18836
  g(70, closed ? 1 : 0);
18676
- for (const [x, y] of p) {
18837
+ for (const [x, y] of tidy(
18838
+ p.map(([x2, y2]) => [x2, height - y2]),
18839
+ closed
18840
+ )) {
18677
18841
  g(0, "VERTEX");
18678
18842
  g(8, layer);
18679
- g(10, n(x));
18680
- g(20, n(height - y));
18843
+ g(10, x);
18844
+ g(20, y);
18681
18845
  g(30, 0);
18682
18846
  }
18683
18847
  g(0, "SEQEND");
@@ -18715,13 +18879,17 @@ function render(source, options = {}) {
18715
18879
  overrides: options.params,
18716
18880
  resolve: options.resolve,
18717
18881
  fonts,
18718
- maxSteps: options.maxSteps
18882
+ maxSteps: options.maxSteps,
18883
+ limits: options.limits
18719
18884
  });
18885
+ const limits = ev.limits;
18720
18886
  let res;
18721
18887
  try {
18722
18888
  res = ev.run(program);
18723
18889
  } catch (e) {
18724
- const err = e instanceof RangeError && /call stack/i.test(e.message) ? new KerfError("Recursion too deep: the program calls itself without stopping") : e;
18890
+ const err = e instanceof RangeError ? new KerfError(
18891
+ /call stack/i.test(e.message) ? "Recursion too deep: the program calls itself without stopping" : "A value grew too large. Is a list or string doubling without end?"
18892
+ ) : e;
18725
18893
  if (err instanceof KerfError) {
18726
18894
  err.echoes = ev.echoes;
18727
18895
  if (fonts?.missing.size) err.missingFonts = [...fonts.missing];
@@ -18778,6 +18946,8 @@ function render(source, options = {}) {
18778
18946
  }
18779
18947
  const b = layerBounds(layers) ?? [0, 0, 0, 0];
18780
18948
  const kerf = options.kerf ?? p.kerf;
18949
+ if (!(Number.isFinite(kerf) && kerf >= 0))
18950
+ throw new KerfError("$kerf must be a number, 0 or more");
18781
18951
  const info = {
18782
18952
  name: p.name,
18783
18953
  qty: p.qty,
@@ -18804,11 +18974,27 @@ function render(source, options = {}) {
18804
18974
  infos.push(info);
18805
18975
  ready.push({ info, layers });
18806
18976
  }
18977
+ let outputPoints = 0;
18978
+ for (const r of ready) {
18979
+ let n2 = 0;
18980
+ for (const l of r.layers.values()) {
18981
+ for (const p of l.regions) n2 += p.length;
18982
+ for (const p of l.lines) n2 += p.length;
18983
+ }
18984
+ outputPoints += n2 * r.info.qty;
18985
+ }
18986
+ if (outputPoints > limits.maxOutputPoints)
18987
+ throw new KerfError(
18988
+ `The files would have ${outputPoints} points, more than the limit of ${limits.maxOutputPoints}. Use fewer copies or simpler shapes.`
18989
+ );
18807
18990
  const sheetSize = options.sheet ?? g("$sheet");
18808
- if (!Array.isArray(sheetSize) || typeof sheetSize[0] !== "number" || typeof sheetSize[1] !== "number")
18809
- throw new KerfError("$sheet must be [width, height]");
18991
+ if (!Array.isArray(sheetSize) || !(Number.isFinite(sheetSize[0]) && sheetSize[0] > 0) || !(Number.isFinite(sheetSize[1]) && sheetSize[1] > 0))
18992
+ throw new KerfError("$sheet must be [width, height], both more than 0");
18810
18993
  const gap = options.gap ?? Number(g("$gap") ?? 3);
18811
18994
  const margin = options.margin ?? gap;
18995
+ if (!(Number.isFinite(gap) && gap >= 0)) throw new KerfError("$gap must be a number, 0 or more");
18996
+ if (!(Number.isFinite(margin) && margin >= 0))
18997
+ throw new KerfError("margin must be a number, 0 or more");
18812
18998
  const layoutMode = options.layout ?? (g("$layout") === "none" ? "none" : "pack");
18813
18999
  const order = [
18814
19000
  "engrave",
@@ -18836,8 +19022,8 @@ function render(source, options = {}) {
18836
19022
  for (const it of items)
18837
19023
  for (const [k, l] of it.layers) {
18838
19024
  const t = all.get(k) ?? { regions: [], lines: [] };
18839
- t.regions.push(...l.regions);
18840
- t.lines.push(...l.lines);
19025
+ for (const p of l.regions) t.regions.push(p);
19026
+ for (const p of l.lines) t.lines.push(p);
18841
19027
  all.set(k, t);
18842
19028
  }
18843
19029
  const b = exportBounds(all);
@@ -18879,6 +19065,10 @@ function render(source, options = {}) {
18879
19065
  let items = [];
18880
19066
  let x = margin, y = margin, shelf = 0, usedW = 0, usedH = 0;
18881
19067
  const flush = () => {
19068
+ if (items.length && sheets.length >= limits.maxSheets)
19069
+ throw new KerfError(
19070
+ `The parts need more than ${limits.maxSheets} sheets, the limit. Use fewer copies or a bigger $sheet.`
19071
+ );
18882
19072
  if (items.length)
18883
19073
  sheets.push(
18884
19074
  finishSheet(
@@ -19037,7 +19227,7 @@ function parameters(source) {
19037
19227
  }
19038
19228
 
19039
19229
  // src/index.ts
19040
- var VERSION = "0.2.0";
19230
+ var VERSION = "0.2.1";
19041
19231
  var LANGUAGE_VERSION = "0.1";
19042
19232
 
19043
19233
  // src/cli.ts