partforge 0.101.0 → 0.103.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/README.md +9 -0
- package/docs/AUTHORING-PARTS.md +15 -3
- package/package.json +1 -1
- package/src/framework/app.css +95 -0
- package/src/framework/chrome.css +75 -1
- package/src/framework/mobile-tabs.js +88 -8
- package/src/framework/mount.js +29 -2
- package/src/framework/panel/author.js +11 -0
- package/src/framework/panel/declared-source.js +128 -0
- package/src/framework/panel/render.js +1 -0
- package/src/framework/panel/widget-specs.js +6 -6
- package/src/framework/panel/widgets/file-drop.js +45 -16
- package/src/framework/panel/widgets/font.js +10 -2
- package/src/framework/panel/widgets/image.js +93 -15
- package/src/framework/panel/widgets/vector-thumb.js +136 -0
- package/src/framework/panel/widgets/vector.js +48 -5
- package/src/framework/rail.js +78 -16
- package/src/parts/emblem.js +22 -9
- package/types/index.d.ts +20 -0
package/src/framework/rail.js
CHANGED
|
@@ -90,12 +90,17 @@ export function attachRail({ rail, toggle, shell = rail?.parentElement, storage
|
|
|
90
90
|
// ?pickserver with no #panel/elements.rail hits this path.
|
|
91
91
|
const root = document.documentElement;
|
|
92
92
|
root.style.setProperty("--pf-rail-w", "0px");
|
|
93
|
-
return { detach: () => root.style.removeProperty("--pf-rail-w") };
|
|
93
|
+
return { layoutChanged: () => {}, detach: () => root.style.removeProperty("--pf-rail-w") };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
const root = document.documentElement;
|
|
97
97
|
const shellBox = () => shell.getBoundingClientRect();
|
|
98
98
|
const shellWidth = () => shellBox().width;
|
|
99
|
+
// A host that leases the rail's placement through attachMobileTabs'
|
|
100
|
+
// setRailLayout writes this ("dock" | "overlay"); absent means partforge's own
|
|
101
|
+
// layout. Read live rather than cached: the lease can move at any moment and
|
|
102
|
+
// layoutChanged() (below) is only a nudge to re-apply, not the source of truth.
|
|
103
|
+
const hostLayout = () => shell.dataset.pfRailLayout ?? null;
|
|
99
104
|
let state = readRailPref(storage, shellWidth());
|
|
100
105
|
// Captured before the first apply() mutates the toggle, so detach() can
|
|
101
106
|
// hand back a plain, unwired button rather than a dead "Show controls" one.
|
|
@@ -142,11 +147,34 @@ export function attachRail({ rail, toggle, shell = rail?.parentElement, storage
|
|
|
142
147
|
// inert, invisible rail. `state` is deliberately NOT rewritten — the stored
|
|
143
148
|
// preference applies again the moment the window widens.
|
|
144
149
|
const narrow = window.innerWidth < RAIL_NARROW_BREAKPOINT;
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
+
// A host layout suspends collapse (and every resize gesture — see
|
|
151
|
+
// resizeRefused) for the same reason narrow does: the rail's placement is
|
|
152
|
+
// the host's to decide, so a collapse flag would only make it inert and
|
|
153
|
+
// invisible where the host is showing it. `state` is left untouched, so
|
|
154
|
+
// the preference returns intact when the lease ends.
|
|
155
|
+
const layout = hostLayout();
|
|
156
|
+
const overlay = layout === "overlay";
|
|
157
|
+
const suppressed = narrow || layout !== null;
|
|
158
|
+
const collapsed = state.collapsed && !suppressed;
|
|
159
|
+
// WIDTH is suppressed on a NARROWER rule than collapse, because only two of
|
|
160
|
+
// the three layouts stop needing --pf-rail-w:
|
|
161
|
+
// - narrow (any layout): chrome.css stacks the panes and shows exactly
|
|
162
|
+
// one, so the rail is the whole surface or absent — it reserves no
|
|
163
|
+
// width beside the viewer;
|
|
164
|
+
// - overlay (any width): the drawer sizes itself (min(288px, 85%)) and
|
|
165
|
+
// floats over the stage, reserving nothing;
|
|
166
|
+
// - dock at >= RAIL_NARROW_BREAKPOINT: the rail is STILL side by side with
|
|
167
|
+
// the stage — the host only leases the bottom inset — so zeroing here
|
|
168
|
+
// would leave the card with no controls at all. Keep the width.
|
|
169
|
+
// Anything centring itself against the rail (app.css's #pf-pick-banner)
|
|
170
|
+
// reads this token, so a stale 288px in the first two cases would offset it.
|
|
171
|
+
// A wide dock therefore renders as: rail visible at its remembered width,
|
|
172
|
+
// no resize, no collapse, and no toggle (hidden just below — a visible one
|
|
173
|
+
// would be a dead control while collapse is suppressed). That is the
|
|
174
|
+
// sheet-owns-layout stance: the host decides placement, we only supply the
|
|
175
|
+
// width it laid the rail out against.
|
|
176
|
+
const widthSuppressed = narrow || overlay;
|
|
177
|
+
const width = collapsed || widthSuppressed ? 0 : clampRailWidth(state.width, sw);
|
|
150
178
|
// Written on :root, not the rail/shell, so body-appended overlays (the
|
|
151
179
|
// pick banner, the ?debug overlay) inherit it — see spec §4.4. This
|
|
152
180
|
// assumes ONE rail per document: attachRail is written for a single
|
|
@@ -162,16 +190,26 @@ export function attachRail({ rail, toggle, shell = rail?.parentElement, storage
|
|
|
162
190
|
seam.setAttribute("aria-valuemax", String(railMaxWidth(sw)));
|
|
163
191
|
if (toggle) {
|
|
164
192
|
// The tab bar owns pane selection below the breakpoint, so a second
|
|
165
|
-
// collapse affordance in the viewbar would be a competing control.
|
|
166
|
-
toggle
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
193
|
+
// collapse affordance in the viewbar would be a competing control. In
|
|
194
|
+
// overlay mode the toggle IS the drawer's open/close control, so it must
|
|
195
|
+
// stay visible even though the width says "narrow" — and the host hides
|
|
196
|
+
// its own tab bar under a layout lease, making this the only way back to
|
|
197
|
+
// the controls. Under dock it stays hidden at EVERY width, including the
|
|
198
|
+
// wide band that keeps its --pf-rail-w above: collapse is suppressed
|
|
199
|
+
// there, so a visible toggle would be a control that does nothing.
|
|
200
|
+
toggle.hidden = suppressed && !overlay;
|
|
201
|
+
// One button, two meanings: the drawer's open state in overlay mode, the
|
|
202
|
+
// collapse everywhere else. `shut` is whichever of the two this layout
|
|
203
|
+
// asks the label, chevron and aria-expanded to report.
|
|
204
|
+
const shut = overlay ? !shell.hasAttribute("data-pf-rail-open") : collapsed;
|
|
205
|
+
toggleChevron?.setAttribute("d", shut ? CHEVRON_RAIL_COLLAPSED : CHEVRON_RAIL_OPEN);
|
|
206
|
+
const label = shut ? "Show controls" : "Hide controls";
|
|
207
|
+
toggle.setAttribute("aria-expanded", String(!shut));
|
|
170
208
|
toggle.setAttribute("aria-label", label);
|
|
171
209
|
// The shared tooltip reads the aria-label at show time, so a native
|
|
172
210
|
// title would double up as a second, competing tooltip.
|
|
173
211
|
if (!tooltipBinding) toggle.title = label;
|
|
174
|
-
toggle.classList.toggle("on",
|
|
212
|
+
toggle.classList.toggle("on", shut);
|
|
175
213
|
tooltipBinding?.sync();
|
|
176
214
|
}
|
|
177
215
|
if (persist) writeRailPref(state, storage);
|
|
@@ -199,10 +237,18 @@ export function attachRail({ rail, toggle, shell = rail?.parentElement, storage
|
|
|
199
237
|
}
|
|
200
238
|
const toggleCollapsed = () => commit({ collapsed: !state.collapsed, width: state.width });
|
|
201
239
|
|
|
240
|
+
// Neither gesture that sizes the rail may run when the seam is not what
|
|
241
|
+
// sizes it: chrome.css hides the vertical seam in the stacked layout (the
|
|
242
|
+
// rail is under the viewer, not beside it), and a host layout sizes the rail
|
|
243
|
+
// itself, where a width or a collapse we wrote would be invisible at best
|
|
244
|
+
// and an inert docked rail at worst. The toggle still works in both.
|
|
245
|
+
const resizeRefused = () => window.innerWidth < RAIL_NARROW_BREAKPOINT || hostLayout() !== null;
|
|
246
|
+
|
|
202
247
|
// --- keyboard: move the SEPARATOR (standard role="separator" semantics), so
|
|
203
248
|
// ArrowLeft widens a right-hand rail. Arrows clamp at the minimum and never
|
|
204
249
|
// collapse; Enter/Space is the collapse gesture.
|
|
205
250
|
function onKeyDown(e) {
|
|
251
|
+
if (resizeRefused()) return;
|
|
206
252
|
// Cmd/Alt/Ctrl+Arrow are browser/OS reserved (back, tab switch, ...); don't
|
|
207
253
|
// eat them just because the seam happens to hold focus.
|
|
208
254
|
if (e.metaKey || e.altKey || e.ctrlKey) return;
|
|
@@ -247,15 +293,26 @@ export function attachRail({ rail, toggle, shell = rail?.parentElement, storage
|
|
|
247
293
|
}
|
|
248
294
|
|
|
249
295
|
const onDoubleClick = () => commit({ collapsed: false, width: RAIL_DEFAULT_WIDTH });
|
|
250
|
-
const onToggleClick = () =>
|
|
296
|
+
const onToggleClick = () => {
|
|
297
|
+
// Overlay: the rail is a drawer over the stage, so the toggle slides it in
|
|
298
|
+
// and out. It must not touch `state` — the collapse preference belongs to
|
|
299
|
+
// the wide layout the host may hand back at any moment, and persisting a
|
|
300
|
+
// drawer gesture into it would collapse the rail on a desktop next visit.
|
|
301
|
+
// The host clears this flag itself when the lease ends or the stage is
|
|
302
|
+
// touched (mobile-tabs.js), so nothing here has to unwind it.
|
|
303
|
+
if (hostLayout() === "overlay") {
|
|
304
|
+
shell.toggleAttribute("data-pf-rail-open");
|
|
305
|
+
apply();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
toggleCollapsed();
|
|
309
|
+
};
|
|
251
310
|
|
|
252
311
|
// --- drag ---
|
|
253
312
|
let grabOffset = 0;
|
|
254
313
|
function onPointerDown(e) {
|
|
255
314
|
if (e.button !== 0) return;
|
|
256
|
-
|
|
257
|
-
// to drag (chrome.css hides it). The toggle still works.
|
|
258
|
-
if (window.innerWidth < RAIL_NARROW_BREAKPOINT) return;
|
|
315
|
+
if (resizeRefused()) return;
|
|
259
316
|
e.preventDefault();
|
|
260
317
|
// setPointerCapture is load-bearing: without it the pointer crosses into the
|
|
261
318
|
// viewer (an iframe, in the cloud editor) whose document eats the move
|
|
@@ -321,6 +378,11 @@ export function attachRail({ rail, toggle, shell = rail?.parentElement, storage
|
|
|
321
378
|
apply();
|
|
322
379
|
|
|
323
380
|
return {
|
|
381
|
+
// The host's data-pf-rail-layout / data-pf-rail-open attributes are read
|
|
382
|
+
// live by apply(), so re-running it is the whole of "the layout moved".
|
|
383
|
+
// Deliberately unconditional: attachMobileTabs reports every setRailLayout
|
|
384
|
+
// call, no-change ones included, and a bare re-apply is idempotent.
|
|
385
|
+
layoutChanged: () => apply(),
|
|
324
386
|
detach: () => {
|
|
325
387
|
settleKeys();
|
|
326
388
|
seam.removeEventListener("pointerdown", onPointerDown);
|
package/src/parts/emblem.js
CHANGED
|
@@ -27,10 +27,14 @@ import plate from "./assets/plate.vector.json" with { type: "json" };
|
|
|
27
27
|
|
|
28
28
|
export default {
|
|
29
29
|
meta: { title: "Emblem", units: "mm", background: 0x15181d },
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Function form, so the `type: "vector"` control below can drive the artwork.
|
|
31
|
+
// The bundled file is the default: dropping an SVG replaces it, clearing the
|
|
32
|
+
// control brings it back, and the part still builds with no network — which is
|
|
33
|
+
// what CI and `partforge measure` see.
|
|
34
|
+
vectors: (p) => ({
|
|
35
|
+
emblem: p.art || new URL("./assets/emblem.vector.json", import.meta.url),
|
|
32
36
|
plate,
|
|
33
|
-
},
|
|
37
|
+
}),
|
|
34
38
|
parameters: [
|
|
35
39
|
{
|
|
36
40
|
id: "plate",
|
|
@@ -45,15 +49,24 @@ export default {
|
|
|
45
49
|
id: "art",
|
|
46
50
|
title: "Artwork",
|
|
47
51
|
description: "The embossed vector art. `emblem.svg` carries a filled circle and a stroked bar, so both of ingest's geometry paths are exercised.",
|
|
48
|
-
advanced:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
// Converted from the legacy `advanced` array to the new shape: a section is
|
|
53
|
+
// one or the other, never both, because mixing them makes render order
|
|
54
|
+
// arbitrary (partforge lint's `mixed-section-shape`). `advanced` becomes a
|
|
55
|
+
// nested group, which renders the same way.
|
|
56
|
+
controls: [
|
|
57
|
+
{ key: "art", type: "vector", label: "Source",
|
|
58
|
+
description: "Drop an SVG to replace the bundled emblem — it is converted to "
|
|
59
|
+
+ "partforge-vector once, in the browser, at drop time. Clear it to go back to the bundled file." },
|
|
60
|
+
{ type: "group", title: "Advanced", collapsed: "auto", controls: [
|
|
61
|
+
{ key: "emblem_w", label: "Emblem width", unit: "mm", min: 8, max: 70, step: 1,
|
|
62
|
+
description: "Width of the artwork's **tight bounding box** in mm — not its `viewBox`. Stroke thickness scales with it." },
|
|
63
|
+
{ key: "emboss", label: "Emboss height", unit: "mm", min: 0.4, max: 4, step: 0.2,
|
|
64
|
+
description: "How far the artwork stands proud of the plate." },
|
|
65
|
+
] },
|
|
53
66
|
],
|
|
54
67
|
},
|
|
55
68
|
],
|
|
56
|
-
defaults: { plate_t: 3, emblem_w: 30, emboss: 1 },
|
|
69
|
+
defaults: { art: "", plate_t: 3, emblem_w: 30, emboss: 1 },
|
|
57
70
|
parts: {
|
|
58
71
|
plate: {
|
|
59
72
|
label: "Plate",
|
package/types/index.d.ts
CHANGED
|
@@ -13,6 +13,18 @@ export * from "./part.js";
|
|
|
13
13
|
/** Which pane a narrow layout shows. `null` hands selection back to partforge. */
|
|
14
14
|
export type HostPane = "stage" | "rail" | null;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Where a HOST wants the controls rail: docked into a bottom sheet the host
|
|
18
|
+
* draws over the frame (`inset` = the sheet's full height, so the stage clears
|
|
19
|
+
* it; `railHeight` = the bottom slice of that region the rail renders into,
|
|
20
|
+
* under the host's own chrome strip), or floated as a right-edge drawer over a
|
|
21
|
+
* full-width stage. `null` hands the layout back to partforge.
|
|
22
|
+
*/
|
|
23
|
+
export type HostRailLayout =
|
|
24
|
+
| { mode: "dock"; inset: number; railHeight?: number }
|
|
25
|
+
| { mode: "overlay" }
|
|
26
|
+
| null;
|
|
27
|
+
|
|
16
28
|
/** An export file format. STEP is routed to OCCT automatically. */
|
|
17
29
|
export type ExportFormat = "stl" | "step" | "3mf";
|
|
18
30
|
|
|
@@ -494,6 +506,14 @@ export interface PartRuntime {
|
|
|
494
506
|
* `null` hands selection back to partforge's built-in bar.
|
|
495
507
|
*/
|
|
496
508
|
setHostPane(pane: HostPane): void;
|
|
509
|
+
/**
|
|
510
|
+
* Where the rail sits, for a host that draws its own chrome over the frame:
|
|
511
|
+
* docked into the host's bottom sheet (`inset` is the sheet's full height,
|
|
512
|
+
* `railHeight` the part of it the rail may paint into), or floated as a
|
|
513
|
+
* right-edge drawer over an unresized stage. `null` — or any shape partforge
|
|
514
|
+
* cannot read — restores partforge's own layout.
|
|
515
|
+
*/
|
|
516
|
+
setRailLayout(layout: HostRailLayout): void;
|
|
497
517
|
/**
|
|
498
518
|
* Part-declared animation playback, or `null` when NO view declares an
|
|
499
519
|
* `animations` block. Non-null while any view does — including while the
|