partforge 0.74.0 → 0.76.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.
@@ -609,7 +609,12 @@ parameters: [
609
609
  ```
610
610
 
611
611
  Every control `key` must exist in `defaults`, or the control is silently dead —
612
- `control-key-not-in-defaults` is an error for exactly that reason.
612
+ `control-key-not-in-defaults` is an error for exactly that reason. Its value there must
613
+ be a **number, string or boolean** — a control writes one scalar, so a control bound to
614
+ an array, an object, `null` or `NaN` is dead in the same silent way
615
+ (`control-default-not-primitive`), and a host that saves panel settings back into
616
+ `defaults` cannot write that value either. A non-primitive `defaults` entry that no
617
+ control is bound to is fine: `defaults` also seeds `p` for `build()`.
613
618
 
614
619
  **Ids.** A section, a group and a preset may carry an `id`; the renderer keys its
615
620
  element, state and disclosure maps on ids, so they must be unique across the whole
@@ -1896,7 +1901,8 @@ about the definition as a whole use `""`.
1896
1901
  behave as authored — whether or not that shows up as a thrown exception. Some error
1897
1902
  findings do correspond to a runtime throw (`build-throws`, `verify-expect-throws`),
1898
1903
  but others catch **silent** wrongness: `missing-meta-title`, `part-view-unknown`,
1899
- `control-key-not-in-defaults`, `preset-key-not-in-defaults`, and
1904
+ `control-key-not-in-defaults`, `control-default-not-primitive`,
1905
+ `preset-key-not-in-defaults`, and
1900
1906
  `verify-unknown-subpart` all fire on parts that build, measure, and verify cleanly —
1901
1907
  a dead control that's silently unreachable, a view that renders nothing, or a
1902
1908
  `verify` expectation that's silently dropped so its gate never runs. That's still an
@@ -1913,7 +1919,8 @@ previously didn't; that's the fix working as intended, not a regression.
1913
1919
  `default-view-ambiguous` (warnings).
1914
1920
 
1915
1921
  **Parameter schema** — `features-requires-sliders`, `features-requires-on`,
1916
- `control-key-not-in-defaults`, `preset-key-not-in-defaults`, `mixed-section-shape`,
1922
+ `control-key-not-in-defaults`, `control-default-not-primitive`,
1923
+ `preset-key-not-in-defaults`, `mixed-section-shape`,
1917
1924
  `duplicate-preset-name`, `duplicate-node-id`, `select-options-missing`,
1918
1925
  `select-default-not-in-options`, `log-scale-needs-positive-min`,
1919
1926
  `when-key-not-in-defaults`, `when-unknown-operator`, `unknown-control-type` (errors);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.74.0",
3
+ "version": "0.76.0",
4
4
  "description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -350,11 +350,20 @@ export function attachAnimationControls(viewer, part, {
350
350
  // even where ResizeObserver is absent.
351
351
  let onStructureChanged = null;
352
352
 
353
+ // Sketch mode takes this slot over: the app floats its own composer where the
354
+ // bar sits, and a frozen sketch over a playing animation is meaningless
355
+ // anyway. Its own flag rather than a straight write to `display`, for the
356
+ // reason mount.js spells out for the view cube's two hide reasons: with one
357
+ // assignment shared between causes, whichever fires last wins, and a view
358
+ // switch back to an animated view would reveal a bar that sketch mode still
359
+ // wants gone.
360
+ let hiddenForSketch = false;
361
+
353
362
  // Per-view + per-animation chrome: which chooser shows, the picker's options,
354
363
  // title, ⓘ description, pager labels, scrubber ticks. A view with no
355
364
  // animations hides the whole bar rather than showing an empty transport.
356
365
  function syncStructure() {
357
- bar.style.display = current ? "" : "none";
366
+ bar.style.display = current && !hiddenForSketch ? "" : "none";
358
367
  onStructureChanged?.();
359
368
  hideChapterBubble();
360
369
  if (!current) return;
@@ -791,6 +800,15 @@ export function attachAnimationControls(viewer, part, {
791
800
  playback?.userEdited();
792
801
  syncUi();
793
802
  },
803
+ // Hide/reveal for SKETCH mode only. `--pf-anim-clear` follows for free:
804
+ // syncStructure notifies the placement pass, which derives the clearance
805
+ // from this same `display` value and publishes 0px for a hidden bar.
806
+ setHidden(hidden) {
807
+ const next = hidden === true;
808
+ if (next === hiddenForSketch) return;
809
+ hiddenForSketch = next;
810
+ syncStructure();
811
+ },
794
812
  // Mount calls this from the view-tab onChange, BEFORE it refreshes the
795
813
  // view: the outgoing animation's params and opacity overrides must be
796
814
  // restored before the incoming view composes its assembly.
@@ -104,6 +104,22 @@ function collectPresetBundles(part) {
104
104
 
105
105
  const defaultKeys = (part) => new Set(Object.keys(part?.defaults ?? {}));
106
106
 
107
+ // What a control can actually edit: one finite scalar. Non-finite numbers are out
108
+ // with the rest — a slider cannot show NaN, and neither NaN nor Infinity survives
109
+ // a round trip through JSON or a source rewrite.
110
+ const isEditableValue = (v) =>
111
+ typeof v === "string" || typeof v === "boolean" || (typeof v === "number" && Number.isFinite(v));
112
+
113
+ // Name a value for a finding's message: the reader needs to know WHICH default is
114
+ // wrong and what it is, and `typeof []` ("object") tells them neither.
115
+ const describeValue = (v) => {
116
+ if (Array.isArray(v)) return "an array";
117
+ if (v === null) return "a null";
118
+ if (typeof v === "number") return `a ${Number.isNaN(v) ? "NaN" : String(v)}`;
119
+ if (v === undefined) return "an undefined";
120
+ return `a ${typeof v}`;
121
+ };
122
+
107
123
  // Walk one WhenCondition, calling onKey(key) for every param key it reads and
108
124
  // onOp(op) for every operator name it uses. allOf/anyOf/not recurse; a bare
109
125
  // `{ key: value }` entry counts as reading `key` with no operator to check.
@@ -191,6 +207,32 @@ export const SCHEMA_RULES = [
191
207
  `${path}.key`));
192
208
  },
193
209
  },
210
+ {
211
+ // A control edits ONE value, and the widgets read and write it as a scalar:
212
+ // a slider over an array renders NaN, a checkbox over an object writes
213
+ // `true` over it. It is dead in the same silent way control-key-not-in-defaults
214
+ // is — the part builds, the panel renders, the control does nothing usable —
215
+ // which is why this is an error rather than a warning.
216
+ //
217
+ // Only a key a CONTROL is bound to. `defaults` also seeds `p` for build(), so
218
+ // parking a table or a point list there for the build to read is legitimate
219
+ // authoring and none of lint's business.
220
+ id: "control-default-not-primitive",
221
+ run: ({ part }) => {
222
+ // Same guard as control-key-not-in-defaults: bail only when `defaults`
223
+ // isn't a plain object at all (missing-defaults already reports that).
224
+ if (!isPlainObject(part?.defaults)) return [];
225
+ const defaults = part.defaults;
226
+ return collectDescriptors(part)
227
+ .filter(({ container }) => !container)
228
+ .filter(({ d }) => typeof d.key === "string" && Object.hasOwn(defaults, d.key))
229
+ .filter(({ d }) => !isEditableValue(defaults[d.key]))
230
+ .map(({ d, path }) => err("control-default-not-primitive",
231
+ `control "${d.key}" is bound to ${describeValue(defaults[d.key])} default`,
232
+ `Give "${d.key}" a number, string or boolean default — a control writes one scalar, so it cannot edit this, and a host that saves panel settings back into \`defaults\` cannot write it either.`,
233
+ `${path}.key`));
234
+ },
235
+ },
194
236
  {
195
237
  id: "preset-key-not-in-defaults",
196
238
  run: ({ part }) => {
@@ -912,6 +912,21 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
912
912
  },
913
913
  });
914
914
  if (animCtl) cleanup.defer(() => animCtl.detach());
915
+ // Sketch mode takes the transport bar's slot: the host floats its own
916
+ // composer there (partforge-cloud's sketch composer), and ink is stored in
917
+ // screen space against the pose it was drawn over — a playing animation
918
+ // under it is meaningless. STOP first, then hide: hiding first leaves a
919
+ // frame where an animation still drives the model beneath a bar that is
920
+ // already gone. Playback does not resume on the way out; "stop" is the
921
+ // contract, and notifyUserEdit is the same pause a user's own control edit
922
+ // performs.
923
+ if (annotateMode && animCtl) {
924
+ cleanup.defer(annotateMode.onModeChange(() => {
925
+ const on = annotateMode.isEnabled();
926
+ if (on) animCtl.notifyUserEdit();
927
+ animCtl.setHidden(on);
928
+ }));
929
+ }
915
930
 
916
931
  // Re-run the active view under the current caching setting, so toggling the
917
932
  // ?debug switch updates the readout for the same design without a param change.