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.
- package/docs/AUTHORING-PARTS.md +454 -80
- package/docs/ERROR-PATTERNS.md +24 -0
- package/package.json +1 -1
- package/src/framework/app.css +114 -18
- package/src/framework/controls.js +1 -1
- package/src/framework/lint/rules-schema.js +359 -21
- package/src/framework/mount.js +18 -3
- package/src/framework/oracle/cases.js +17 -6
- package/src/framework/panel/author.js +88 -0
- package/src/framework/panel/info.js +10 -1
- package/src/framework/panel/legacy.js +9 -1
- package/src/framework/panel/render.js +85 -19
- package/src/framework/panel/widget-specs.js +58 -0
- package/src/framework/panel/widgets/checkbox.js +2 -1
- package/src/framework/panel/widgets/index.js +3 -0
- package/src/framework/panel/widgets/numeric.js +69 -8
- package/src/framework/panel/widgets/readout.js +31 -0
- package/src/framework/panel/widgets/select.js +73 -0
- package/src/framework/panel/widgets/text.js +2 -1
- package/src/parts/bracket.js +5 -5
- package/src/parts/planter.js +23 -17
- package/types/part.d.ts +96 -5
|
@@ -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] ?? ""); };
|
package/src/parts/bracket.js
CHANGED
|
@@ -42,12 +42,12 @@ export default {
|
|
|
42
42
|
{
|
|
43
43
|
id: "shape",
|
|
44
44
|
title: "Shape ops",
|
|
45
|
-
|
|
46
|
-
{ key: "clip",
|
|
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
|
-
|
|
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
|
},
|
package/src/parts/planter.js
CHANGED
|
@@ -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
|
|
34
|
-
"**Facets** and **Twist** are pure styling; **Wall**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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: "
|
|
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
|
-
{
|
|
54
|
-
|
|
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
|
|
75
|
-
*
|
|
76
|
-
* ignored by the panel and
|
|
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
|
|