partforge 0.20.1 → 0.20.2
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
CHANGED
|
@@ -316,24 +316,36 @@ changing any of them is a fresh cache node while an identical rebuild is a hit.
|
|
|
316
316
|
`parameters` is an **array of sections**; the framework builds the panel from it and
|
|
317
317
|
binds each control to a key in `defaults`. Two section kinds:
|
|
318
318
|
|
|
319
|
-
**Preset +
|
|
319
|
+
**Preset + controls section:**
|
|
320
320
|
|
|
321
321
|
```js
|
|
322
322
|
{
|
|
323
323
|
id: "body",
|
|
324
324
|
title: "Body",
|
|
325
325
|
presets: { M3: { od: 8, bore: 3.4, h: 10 }, M5: { od: 12, bore: 5.4, h: 16 } }, // name → param overrides
|
|
326
|
-
advanced: [ //
|
|
326
|
+
advanced: [ // controls revealed under "Advanced"
|
|
327
327
|
{ key: "od", label: "Outer diameter", unit: "mm", min: 4, max: 40, step: 0.5 },
|
|
328
328
|
{ key: "bore", label: "Bore", unit: "mm", min: 1, max: 30, step: 0.1, control: "number" },
|
|
329
|
+
{ key: "title", label: "Title", control: "text" },
|
|
330
|
+
{ key: "label", label: "Label", control: "textarea" },
|
|
329
331
|
],
|
|
330
332
|
}
|
|
331
333
|
```
|
|
332
334
|
|
|
333
|
-
|
|
335
|
+
Numeric slider/feature controls show an **editable number box** beside them — drag the
|
|
334
336
|
slider or type an exact value (finer than `step` is allowed; typed values clamp to
|
|
335
|
-
`[min, max]`). Optional `control` per parameter
|
|
336
|
-
|
|
337
|
+
`[min, max]`). Optional `control` per parameter chooses the input:
|
|
338
|
+
|
|
339
|
+
- omit it (or use `"slider"`) for a slider + number box;
|
|
340
|
+
- `"number"` for a number box only (handy for precise or wide-range values);
|
|
341
|
+
- `"text"` for a single-line string field;
|
|
342
|
+
- `"textarea"` for a multiline string field whose line breaks are preserved.
|
|
343
|
+
|
|
344
|
+
Text fields update `params` live on every edit, so the existing rebuild loop previews
|
|
345
|
+
the new string immediately. Give every text key a string value in `defaults`; empty
|
|
346
|
+
strings are valid control values, while the part's build function decides whether its
|
|
347
|
+
geometry supports them. Editing any control in a preset section selects `Custom`, and
|
|
348
|
+
choosing a preset updates both numeric and text fields.
|
|
337
349
|
|
|
338
350
|
**Feature-toggle section** (checkbox enables a feature + reveals its sliders; `0` = off):
|
|
339
351
|
|
package/package.json
CHANGED
package/src/framework/app.css
CHANGED
|
@@ -81,6 +81,14 @@ select.preset {
|
|
|
81
81
|
.row .num::-webkit-outer-spin-button, .row .num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
82
82
|
.row .num { -moz-appearance: textfield; }
|
|
83
83
|
.row .unit { font-family: var(--pf-mono); color: var(--pf-muted); font-size: 10px; }
|
|
84
|
+
.text-input {
|
|
85
|
+
display: block; width: 100%; font: 12px/1.4 var(--pf-mono);
|
|
86
|
+
background: var(--pf-input-bg); color: var(--pf-text-strong);
|
|
87
|
+
border: 1px solid var(--pf-border); border-radius: 6px; padding: 6px 8px;
|
|
88
|
+
}
|
|
89
|
+
textarea.text-input { min-height: 64px; resize: vertical; }
|
|
90
|
+
.text-input:focus { outline: none; border-color: var(--pf-accent);
|
|
91
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
|
|
84
92
|
|
|
85
93
|
/* crafted range slider — hairline track + CAD-blue handle (the panel's signature control) */
|
|
86
94
|
input[type="range"] { -webkit-appearance: none; appearance: none; width: 100%; height: 18px; margin: 0; background: transparent; cursor: pointer; }
|
|
@@ -113,6 +113,8 @@ function el(tag, className, text) {
|
|
|
113
113
|
// One parameter control bound to params[def.key]. `def.control`:
|
|
114
114
|
// "slider" (default) — range slider + an editable number box (drag OR type)
|
|
115
115
|
// "number" — number box only (no slider)
|
|
116
|
+
// "text" — single-line text field
|
|
117
|
+
// "textarea" — multiline text field
|
|
116
118
|
// The box accepts exact values (finer than `step`); typed values clamp to
|
|
117
119
|
// [min, max] on commit (blur/Enter). Returns { wrap, sync }.
|
|
118
120
|
function makeSlider(def, params, onChange, info) {
|
|
@@ -173,6 +175,34 @@ function makeSlider(def, params, onChange, info) {
|
|
|
173
175
|
return { wrap, sync };
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
function makeTextControl(def, params, onChange, info) {
|
|
179
|
+
const multiline = def.control === "textarea";
|
|
180
|
+
const wrap = el("div", "slider");
|
|
181
|
+
const row = el("div", "row");
|
|
182
|
+
const label = el("label", "", def.label);
|
|
183
|
+
attachInfo(label, def.description, info);
|
|
184
|
+
row.append(label);
|
|
185
|
+
wrap.append(row);
|
|
186
|
+
|
|
187
|
+
const field = document.createElement(multiline ? "textarea" : "input");
|
|
188
|
+
if (!multiline) field.type = "text";
|
|
189
|
+
field.className = "text-input";
|
|
190
|
+
field.value = String(params[def.key] ?? "");
|
|
191
|
+
field.addEventListener("input", () => {
|
|
192
|
+
params[def.key] = field.value;
|
|
193
|
+
onChange?.();
|
|
194
|
+
});
|
|
195
|
+
wrap.append(field);
|
|
196
|
+
|
|
197
|
+
const sync = () => { field.value = String(params[def.key] ?? ""); };
|
|
198
|
+
return { wrap, sync };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const makeParameterControl = (def, params, onChange, info) =>
|
|
202
|
+
def.control === "text" || def.control === "textarea"
|
|
203
|
+
? makeTextControl(def, params, onChange, info)
|
|
204
|
+
: makeSlider(def, params, onChange, info);
|
|
205
|
+
|
|
176
206
|
// A collapsible "Advanced ▾" block. Returns { adv, toggle }.
|
|
177
207
|
function advancedBlock() {
|
|
178
208
|
const adv = el("div", "adv hidden");
|
|
@@ -242,7 +272,7 @@ function buildPresetSection(section, sec, params, onDirty, register, info) {
|
|
|
242
272
|
if (advanced.length) {
|
|
243
273
|
const { adv, toggle } = advancedBlock();
|
|
244
274
|
for (const def of advanced) {
|
|
245
|
-
const s =
|
|
275
|
+
const s = makeParameterControl(def, params, () => { if (preset) preset.value = "Custom"; onDirty?.(); }, info);
|
|
246
276
|
adv.append(s.wrap);
|
|
247
277
|
syncs[def.key] = s.sync;
|
|
248
278
|
register(def.key, s.wrap);
|
|
@@ -281,7 +311,7 @@ function buildFeatureSection(section, sec, params, onDirty, register, info) {
|
|
|
281
311
|
const group = el("div", "feat-group");
|
|
282
312
|
const syncs = [];
|
|
283
313
|
for (const def of feat.sliders.filter((d) => !d.hidden)) {
|
|
284
|
-
const s =
|
|
314
|
+
const s = makeParameterControl(def, params, onDirty, info);
|
|
285
315
|
group.append(s.wrap);
|
|
286
316
|
syncs.push(s.sync);
|
|
287
317
|
register(def.key, s.wrap);
|
|
@@ -287,6 +287,11 @@ export function createCutawayGizmo({
|
|
|
287
287
|
|
|
288
288
|
function pick(event, ray) {
|
|
289
289
|
if (pickHandle) return resolveHandle(pickHandle(event, handles, ray));
|
|
290
|
+
handleRoot.updateWorldMatrix(true, true);
|
|
291
|
+
const intersection = raycaster.intersectObjects(hitProxies, false)[0];
|
|
292
|
+
const intersectedHandle = resolveHandle(intersection);
|
|
293
|
+
if (intersectedHandle) return intersectedHandle;
|
|
294
|
+
|
|
290
295
|
const center = projectToClient(group.position);
|
|
291
296
|
if (center) {
|
|
292
297
|
const dx = event.clientX - center.x;
|
|
@@ -296,9 +301,7 @@ export function createCutawayGizmo({
|
|
|
296
301
|
return "translate";
|
|
297
302
|
}
|
|
298
303
|
}
|
|
299
|
-
|
|
300
|
-
const intersection = raycaster.intersectObjects(hitProxies, false)[0];
|
|
301
|
-
return resolveHandle(intersection);
|
|
304
|
+
return null;
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
function safeCapture(pointerId) {
|
package/src/parts/nameplate.js
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
// exercises shape2d + the extrude/boolean path. Open /nameplate.html after `npm run dev`.
|
|
6
6
|
import { roundedRectPolygon } from "partforge/geometry";
|
|
7
7
|
|
|
8
|
-
const LABEL = "PARTFORGE\nv0.20";
|
|
9
|
-
|
|
10
8
|
export default {
|
|
11
9
|
meta: { title: "Nameplate", units: "mm", background: 0x15181d },
|
|
12
10
|
parameters: [
|
|
13
11
|
{
|
|
14
12
|
id: "text",
|
|
15
13
|
title: "Lettering",
|
|
16
|
-
description: "
|
|
14
|
+
description: "Editable text resolved to exact glyph curves, with open counters and sizing based on cap height.",
|
|
17
15
|
advanced: [
|
|
16
|
+
{ key: "label", label: "Text", control: "textarea",
|
|
17
|
+
description: "The text rendered on the nameplate. Line breaks create multiple lines." },
|
|
18
18
|
{ key: "size", label: "Cap height", unit: "mm", min: 4, max: 16, step: 0.5,
|
|
19
19
|
description: "Height of the uppercase letters. The second line scales with it." },
|
|
20
20
|
{ key: "depth", label: "Relief depth", unit: "mm", min: 0.4, max: 3, step: 0.1,
|
|
@@ -45,14 +45,14 @@ export default {
|
|
|
45
45
|
],
|
|
46
46
|
},
|
|
47
47
|
],
|
|
48
|
-
defaults: { size: 8, depth: 1.2, stroke: 0, margin: 4, corner: 3, thickness: 3, engrave: 0 },
|
|
48
|
+
defaults: { label: "PARTFORGE\nv0.20", size: 8, depth: 1.2, stroke: 0, margin: 4, corner: 3, thickness: 3, engrave: 0 },
|
|
49
49
|
parts: {
|
|
50
50
|
plate: {
|
|
51
51
|
label: "Nameplate",
|
|
52
52
|
views: ["plate"],
|
|
53
53
|
export: { name: "nameplate" },
|
|
54
54
|
build: (k, p) => {
|
|
55
|
-
let text = k.text2d(
|
|
55
|
+
let text = k.text2d(p.label, { size: p.size, align: "center", valign: "middle", lineHeight: p.size * 1.7 });
|
|
56
56
|
// Shape2D offset on the lettering: grow (>0, bolder) or shrink (<0, thinner). Guard
|
|
57
57
|
// against a shrink that collapses thin strokes — keep the un-offset letters if so.
|
|
58
58
|
if (p.stroke !== 0) {
|