partforge 0.47.1 → 0.49.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.
@@ -0,0 +1,73 @@
1
+ // select: a dropdown over `options`. radio: the same data as a segmented
2
+ // control (reuses the app's existing `.seg` styling), for 2–4 options where
3
+ // seeing all of them matters. Option values may be strings or numbers; the DOM
4
+ // only speaks strings, so both widgets map String(value) back to the real
5
+ // value on the way out.
6
+ import { attachInfo } from "../info.js";
7
+ import { normalizeOptions } from "../widget-specs.js";
8
+
9
+ function el(tag, className, text) {
10
+ const node = document.createElement(tag);
11
+ if (className) node.className = className;
12
+ if (text != null) node.textContent = text;
13
+ return node;
14
+ }
15
+
16
+ function labeledRow(node, info) {
17
+ const wrap = el("div", "slider");
18
+ const row = el("div", "row");
19
+ const label = el("label", "", node.label);
20
+ attachInfo(label, node.description, info);
21
+ row.append(label);
22
+ wrap.append(row);
23
+ return wrap;
24
+ }
25
+
26
+ export function makeSelect(node, params, { onChange, onCommit, info }) {
27
+ const wrap = labeledRow(node, info);
28
+ const opts = normalizeOptions(node.options);
29
+ const byString = new Map(opts.map((o) => [String(o.value), o.value]));
30
+ const select = document.createElement("select");
31
+ select.className = "select-input";
32
+ for (const o of opts) {
33
+ const opt = document.createElement("option");
34
+ opt.value = String(o.value);
35
+ opt.textContent = o.label;
36
+ if (o.description) opt.title = o.description; // long-form option descriptions surface as tooltips
37
+ select.append(opt);
38
+ }
39
+ select.value = String(params[node.key]);
40
+ select.addEventListener("change", () => {
41
+ params[node.key] = byString.get(select.value);
42
+ onChange?.();
43
+ onCommit?.();
44
+ });
45
+ wrap.append(select);
46
+ const sync = () => { select.value = String(params[node.key]); };
47
+ return { el: wrap, sync };
48
+ }
49
+
50
+ export function makeRadio(node, params, { onChange, onCommit, info }) {
51
+ const wrap = labeledRow(node, info);
52
+ const opts = normalizeOptions(node.options);
53
+ const seg = el("div", "seg");
54
+ const buttons = opts.map((o) => {
55
+ const b = el("button", "", o.label);
56
+ b.type = "button";
57
+ if (o.description) b.title = o.description;
58
+ b.addEventListener("click", () => {
59
+ params[node.key] = o.value;
60
+ paint();
61
+ onChange?.();
62
+ onCommit?.();
63
+ });
64
+ seg.append(b);
65
+ return { b, value: o.value };
66
+ });
67
+ const paint = () => {
68
+ for (const { b, value } of buttons) b.classList.toggle("on", params[node.key] === value);
69
+ };
70
+ paint();
71
+ wrap.append(seg);
72
+ return { el: wrap, sync: paint };
73
+ }
@@ -9,7 +9,7 @@ function el(tag, className, text) {
9
9
  return node;
10
10
  }
11
11
 
12
- export function makeText(node, params, { onChange, info }) {
12
+ export function makeText(node, params, { onChange, onCommit, info }) {
13
13
  const multiline = node.type === "textarea";
14
14
  const wrap = el("div", "slider");
15
15
  const row = el("div", "row");
@@ -26,6 +26,7 @@ export function makeText(node, params, { onChange, info }) {
26
26
  params[node.key] = field.value;
27
27
  onChange?.();
28
28
  });
29
+ field.addEventListener("change", () => onCommit?.());
29
30
  wrap.append(field);
30
31
 
31
32
  const sync = () => { field.value = String(params[node.key] ?? ""); };
@@ -42,12 +42,12 @@ export default {
42
42
  {
43
43
  id: "shape",
44
44
  title: "Shape ops",
45
- toggles: [
46
- { key: "clip", label: "Clip arms to a disc (intersect)", on: 1,
45
+ controls: [
46
+ { key: "clip", type: "radio", label: "Arm tips",
47
+ options: [{ value: 0, label: "Square" }, { value: 1, label: "Clipped" }],
47
48
  description: "**Intersect** the cross with a circle so the four arm tips are rounded off to a common radius." },
48
- ],
49
- advanced: [
50
- { key: "clearance", label: "Print-clearance offset", unit: "mm", min: 0, max: 1, step: 0.1,
49
+ { key: "clearance", type: "slider", label: "Print-clearance offset", unit: "mm",
50
+ min: 0, max: 1, step: 0.1,
51
51
  description: "**Offset** the whole outline outward (round corners) for a looser slip fit. 0 = none." },
52
52
  ],
53
53
  },
@@ -30,28 +30,33 @@ export default {
30
30
  id: "body",
31
31
  title: "Body",
32
32
  description:
33
- "The faceted vessel. Pick a preset to start, or open **Advanced** for exact dimensions. " +
34
- "**Facets** and **Twist** are pure styling; **Wall** is the one that decides whether it prints cleanly.",
35
- presets: {
36
- "Pen cup": { facets: 6, dia: 80, height: 100, taper: 1.0, twist: 0, drain: 0 },
37
- Planter: { facets: 8, dia: 90, height: 80, taper: 0.9, twist: 0, drain: 8 },
38
- Vase: { facets: 5, dia: 70, height: 150, taper: 1.12, twist: 40, drain: 0 },
39
- },
40
- advanced: [
41
- { key: "facets", label: "Facets", min: 3, max: 12, step: 1,
33
+ "The faceted vessel. Pick a preset to start, or dial exact dimensions below — " +
34
+ "**Facets** and **Twist** are pure styling; open **Wall** for the one that decides whether it prints cleanly.",
35
+ controls: [
36
+ { type: "preset", presets: {
37
+ "Pen cup": { facets: 6, dia: 80, height: 100, taper: 1.0, twist: 0, drain: 0 },
38
+ Planter: { facets: 8, dia: 90, height: 80, taper: 0.9, twist: 0, drain: 8 },
39
+ Vase: { facets: 5, dia: 70, height: 150, taper: 1.12, twist: 40, drain: 0 },
40
+ } },
41
+ { key: "facets", type: "slider", label: "Facets", min: 3, max: 12, step: 1,
42
42
  description: "Number of flat sides around the body. Low counts read as crystalline; high counts approach a smooth cylinder." },
43
- { key: "dia", label: "Diameter", unit: "mm", min: 30, max: 150, step: 1,
43
+ { key: "dia", type: "slider", label: "Diameter", unit: "mm", min: 30, max: 150, step: 1,
44
44
  description: "Across-corners diameter at the base. Size it to the plant, pens, or shelf it has to fit." },
45
- { key: "height", label: "Height", unit: "mm", min: 20, max: 200, step: 1,
45
+ { key: "height", type: "slider", label: "Height", unit: "mm", min: 20, max: 200, step: 1,
46
46
  description: "Overall height along the axis." },
47
- { key: "taper", label: "Top taper", min: 0.6, max: 1.4, step: 0.02,
47
+ { key: "taper", type: "slider", label: "Top taper", min: 0.6, max: 1.4, step: 0.02,
48
48
  description: "Rim size relative to the base: below 1 tapers inward (planter), 1 is straight (cup), above 1 flares out (vase)." },
49
- { key: "wall", label: "Wall thickness", unit: "mm", min: 0.8, max: 4, step: 0.1,
50
- description: "Side-wall thickness. The fdm-pla profile wants **≥ 1.2 mm** — go thinner and partforge flags a min-wall warning." },
51
- { key: "twist", label: "Twist", unit: "°", min: 0, max: 180, step: 5,
49
+ { key: "twist", type: "slider", label: "Twist", unit: "°", min: 0, max: 180, step: 5,
52
50
  description: "Rotates the facets from base to rim for a spiral look. 0 keeps the facets vertical." },
53
- { key: "floor", label: "Floor thickness", unit: "mm", min: 1, max: 6, step: 0.5, hidden: true,
54
- description: "Internal: solid base thickness, fixed by the design. Hidden from the end user but still drives the geometry." },
51
+ { type: "group", title: "Wall", collapsed: "auto", controls: [
52
+ { key: "wall", type: "slider", label: "Wall thickness", unit: "mm", min: 0.8, max: 4, step: 0.1,
53
+ recommended: [1.2, 4],
54
+ description: "Side-wall thickness. The fdm-pla profile wants **≥ 1.2 mm** — go thinner and partforge flags a min-wall warning." },
55
+ { type: "readout", label: "Inner diameter", derivedKey: "innerDia", unit: "mm",
56
+ description: "Clear inside width at the base, after the walls." },
57
+ { key: "floor", type: "slider", label: "Floor thickness", unit: "mm", min: 1, max: 6, step: 0.5, hidden: true,
58
+ description: "Internal: solid base thickness, fixed by the design. Hidden from the end user but still drives the geometry." },
59
+ ] },
55
60
  ],
56
61
  },
57
62
  {
@@ -87,6 +92,7 @@ export default {
87
92
  // pick it so inner_radius(top) = outer_radius(top) − wall.
88
93
  innerTaper: 1 + (Rout * (p.taper - 1)) / Rin,
89
94
  drainR: (p.drain + 0.2) / 2, // nominal hole + 0.2 mm print clearance, as a radius
95
+ innerDia: 2 * Rin, // across-corners inner diameter at the base, after the wall inset
90
96
  };
91
97
  },
92
98
  parts: {
package/types/part.d.ts CHANGED
@@ -58,7 +58,7 @@ export interface PartMeta {
58
58
  export type ControlKind = "slider" | "number" | "text" | "textarea";
59
59
 
60
60
  /** Every control type the panel can render. */
61
- export type ControlType = "slider" | "number" | "text" | "textarea" | "checkbox";
61
+ export type ControlType = "slider" | "number" | "text" | "textarea" | "checkbox" | "select" | "radio";
62
62
 
63
63
  /** A declarative visibility condition, evaluated against raw parameters. */
64
64
  export type WhenCondition =
@@ -70,10 +70,84 @@ export type WhenCondition =
70
70
  ne?: ParamValue; in?: ParamValue[];
71
71
  }>;
72
72
 
73
+ /** One entry in a `controls` array: a control, a nested group, a preset picker, or a readout. */
74
+ export type PanelEntry = PanelControlEntry | PanelGroupEntry | PanelPresetEntry | PanelReadoutEntry;
75
+
76
+ /** A control bound to one key in `defaults`. `type` defaults to `"slider"`. */
77
+ export interface PanelControlEntry {
78
+ key: string;
79
+ type?: ControlType;
80
+ label?: string;
81
+ description?: string;
82
+ unit?: string;
83
+ min?: number;
84
+ max?: number;
85
+ step?: number;
86
+ /** checkbox: the value written when ticked (default 1). */
87
+ on?: number;
88
+ /** select / radio: the choices. Strings are both value and label. */
89
+ options?: Array<ParamValue | { value: ParamValue; label?: string; description?: string }>;
90
+ /** slider: logarithmic response. Requires min > 0. */
91
+ scale?: "log";
92
+ /** slider: marked values on the track; `snap: true` makes the thumb prefer them. */
93
+ ticks?: number[];
94
+ snap?: boolean;
95
+ /** slider: [lo, hi] band drawn on the track; outside it the value box takes a warning tint. */
96
+ recommended?: [number, number];
97
+ hidden?: boolean;
98
+ when?: WhenCondition;
99
+ whenFalse?: "disable";
100
+ /** These two discriminate against the container entries. */
101
+ controls?: undefined;
102
+ presets?: undefined;
103
+ }
104
+
105
+ /** A nested group. `collapsed` defaults to `"auto"` (the small-panel auto-open rule). */
106
+ export interface PanelGroupEntry {
107
+ type: "group";
108
+ id?: string;
109
+ title?: string;
110
+ collapsed?: boolean | "auto";
111
+ /** No title, no disclosure — just an indented block. */
112
+ bare?: boolean;
113
+ controls: PanelEntry[];
114
+ hidden?: boolean;
115
+ when?: WhenCondition;
116
+ whenFalse?: "disable";
117
+ }
118
+
119
+ /** A preset picker, positionable anywhere among the controls. */
120
+ export interface PanelPresetEntry {
121
+ type: "preset";
122
+ id?: string;
123
+ label?: string;
124
+ /** Preset name -> the param overrides it applies. */
125
+ presets: Record<string, Record<string, ParamValue>>;
126
+ hidden?: boolean;
127
+ when?: WhenCondition;
128
+ whenFalse?: "disable";
129
+ }
130
+
131
+ /** A read-only display of one `derive()` output. Not bound to `defaults`. */
132
+ export interface PanelReadoutEntry {
133
+ type: "readout";
134
+ label?: string;
135
+ description?: string;
136
+ unit?: string;
137
+ derivedKey: string;
138
+ hidden?: boolean;
139
+ when?: WhenCondition;
140
+ whenFalse?: "disable";
141
+ key?: undefined;
142
+ controls?: undefined;
143
+ presets?: undefined;
144
+ }
145
+
73
146
  /**
74
- * One parameter control. The recognised field list mirrors
75
- * `CONTROL_FIELDS` in src/framework/lint/rules-schema.js anything else is
76
- * ignored by the panel and warned about by `partforge lint`.
147
+ * One parameter control in a legacy `advanced` / `sliders` array. The recognised
148
+ * field list is the registry's, `fieldsFor("slider")` in
149
+ * src/framework/panel/widget-specs.js — anything else is ignored by the panel and
150
+ * warned about by `partforge lint`.
77
151
  *
78
152
  * @deprecated Prefer a `controls` array of control nodes. Still fully supported.
79
153
  */
@@ -142,14 +216,31 @@ export interface PresetSection extends SectionBase {
142
216
  advanced?: ControlDef[];
143
217
  /** A section with `features` is a feature section; `advanced` is ignored there. */
144
218
  features?: undefined;
219
+ /** Discriminator: a PresetSection carries no `controls` array. */
220
+ controls?: undefined;
145
221
  }
146
222
 
147
223
  /** A feature-toggle section: each feature is a checkbox plus its own controls. */
148
224
  export interface FeatureSection extends SectionBase {
149
225
  features: FeatureDef[];
226
+ /** Discriminator: a FeatureSection carries no `controls` array. */
227
+ controls?: undefined;
228
+ }
229
+
230
+ /** The new section shape: everything in `controls`, in render order. */
231
+ export interface NodeSection extends SectionBase {
232
+ controls: PanelEntry[];
233
+ collapsed?: boolean | "auto";
234
+ when?: WhenCondition;
235
+ whenFalse?: "disable";
236
+ /** Discriminators: a NodeSection carries none of the legacy arrays. */
237
+ features?: undefined;
238
+ advanced?: undefined;
239
+ toggles?: undefined;
240
+ presets?: undefined;
150
241
  }
151
242
 
152
- export type ParameterSection = PresetSection | FeatureSection;
243
+ export type ParameterSection = PresetSection | FeatureSection | NodeSection;
153
244
 
154
245
  // --- fonts ------------------------------------------------------------------
155
246