partforge 0.103.0 → 0.104.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.
@@ -2057,8 +2057,17 @@ see "Wiring a part into a runnable app", above, for its full signature):
2057
2057
  **The `allow` list, and what bypasses it.** All three controls take the same
2058
2058
  `allow` list already documented for `"font"` in the control-types table above
2059
2059
  (`"https"` — the default; `"gstatic"`, font-only, `https://fonts.gstatic.com`
2060
- exactly; `"asset"`, a `pfc-asset://` token the host has stored for this part),
2061
- gating what a **param-supplied** value may be. It never restricts a source an
2060
+ exactly; `"asset"`, a `pfc-asset://` token the host has stored for this part;
2061
+ `"tree"`, vector-only, a `pfc-tree://` token naming artwork that lives as a
2062
+ **file inside the part itself** rather than in host storage), gating what a
2063
+ **param-supplied** value may be.
2064
+
2065
+ `"tree"` is vector-only by construction, not by omission: a part's files are
2066
+ text, so a `partforge-vector` JSON document can live in one and a PNG or a
2067
+ font cannot. Its payoff is that the artwork is versioned *with* the part —
2068
+ a host that stores parts as a file tree keeps the document in the same
2069
+ snapshot as the code that reads it, so publishing, history and undo carry the
2070
+ two together instead of leaving a param pointing at storage that moved on. It never restricts a source an
2062
2071
  author writes into `images`/`fonts`/`vectors` themselves — that's code, not
2063
2072
  user input.
2064
2073
 
@@ -2338,14 +2347,17 @@ yourself (e.g. to download from a different origin) instead of partforge's own D
2338
2347
  where `ImageAsset` is `{ id, label, url, width, height, thumbUrl }`. With no
2339
2348
  provider a `type: "image"` control degrades to a URL field.
2340
2349
 
2341
- - `onAssetUpload(blob, { kind, filename }) → Promise<string>` — the drop target
2342
- shared by `"image"`/`"vector"`/`"font"` controls (see "Getting files into a
2343
- part", below) calls this with the converted artifact after a drop, paste, or
2344
- file-picker choice, and writes whatever it resolves to into the param.
2345
- `blob` is the CONVERTED artifact — a PNG, a partforge-vector JSON blob, or the
2346
- original file for a font — never the user's raw drop; `kind` is `"image"`,
2347
- `"vector"`, or `"font"`. Must resolve to a non-empty source string (an
2348
- `https:` URL or a host-defined `pfc-asset:` token); anything else is treated
2350
+ - `onAssetUpload(blob, { kind, key, filename }) → Promise<string>` — the drop
2351
+ target shared by `"image"`/`"vector"`/`"font"` controls (see "Getting files
2352
+ into a part", below) calls this with the converted artifact after a drop,
2353
+ paste, or file-picker choice, and writes whatever it resolves to into the
2354
+ param. `blob` is the CONVERTED artifact — a PNG, a partforge-vector JSON blob,
2355
+ or the original file for a font — never the user's raw drop; `kind` is
2356
+ `"image"`, `"vector"`, or `"font"`; `key` is the param the drop landed on,
2357
+ which is what lets a host give each control a stable destination of its own
2358
+ rather than reconciling uploads against the part afterwards. Must resolve to
2359
+ a non-empty source string (an `https:` URL, or a host-defined `pfc-asset:` or
2360
+ `pfc-tree:` token); anything else is treated
2349
2361
  as a contract violation and reported through the control's own error line,
2350
2362
  not written into the param. Omit it and the converted bytes land straight in
2351
2363
  the param instead — the path a host that cannot fetch URLs (the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.103.0",
3
+ "version": "0.104.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",
@@ -96,7 +96,7 @@ function el(tag, className, text) {
96
96
  // The click path is dropped rather than hidden, deliberately: an invisible
97
97
  // overlay that still swallowed clicks would eat the button underneath it, which
98
98
  // is the one affordance ambient mode exists to protect.
99
- export function makeFileDrop({ kind, onSource, onError, onAssetUpload, ambient = false }) {
99
+ export function makeFileDrop({ kind, key, onSource, onError, onAssetUpload, ambient = false }) {
100
100
  const row = rowFor(kind);
101
101
  const wrap = el("div", ambient ? "file-drop file-drop-ambient" : "file-drop");
102
102
  if (!ambient) {
@@ -219,7 +219,14 @@ export function makeFileDrop({ kind, onSource, onError, onAssetUpload, ambient =
219
219
 
220
220
  if (onAssetUpload) {
221
221
  try {
222
- const source = await onAssetUpload(artifact.blob, { kind, filename: file.name });
222
+ // `key` is the PARAM the drop landed on. A host needs it to give a
223
+ // control a stable destination of its own — partforge-cloud derives
224
+ // one file path per key, so re-dropping replaces that control's
225
+ // artwork in place instead of accumulating files it must later
226
+ // reconcile against the part. Purely informational to partforge:
227
+ // the hook still answers with a source string and nothing here
228
+ // reads the key back.
229
+ const source = await onAssetUpload(artifact.blob, { kind, key, filename: file.name });
223
230
  if (stale()) return;
224
231
  // A host hook that resolves to anything but a non-empty string —
225
232
  // `undefined`, an object, `""` — is a contract violation, not a
@@ -329,6 +336,7 @@ export function mountDrop(kind, { params, node, onAssetUpload, onChange, onCommi
329
336
 
330
337
  const drop = makeFileDrop({
331
338
  kind,
339
+ key: node.key,
332
340
  ambient,
333
341
  onAssetUpload,
334
342
  onSource: (source) => {
@@ -52,6 +52,14 @@ import { unwrapModule } from "./asset-resolve.js";
52
52
  export const VECTOR_ALLOW_DEFAULT = ["https"];
53
53
 
54
54
  const ASSET_SCHEME = "pfc-asset:";
55
+ // Artwork that lives as a FILE IN THE PART ITSELF, addressed by its path
56
+ // rather than by a host-side storage id. Vector-only, and the asymmetry is
57
+ // structural rather than an omission: a part's files are text, so a JSON
58
+ // document can live in one and a PNG cannot. A host that stores parts as a
59
+ // file tree (partforge-cloud is the motivating case) resolves this scheme
60
+ // against that tree; partforge itself only decides whether a param may carry
61
+ // it, exactly as it does for `pfc-asset:`.
62
+ const TREE_SCHEME = "pfc-tree:";
55
63
 
56
64
  // The "unset" vector source. An empty value declares NO artwork for that
57
65
  // name — mirrors isNoFontSource/isNoImageSource exactly. Never a source to
@@ -94,6 +102,7 @@ export function vectorSourceAllowed(source, allow = VECTOR_ALLOW_DEFAULT) {
94
102
  // "pfc-asset://" must not pass, and neither must a lookalike host.
95
103
  if (kind === "https" && u.protocol === "https:") return true;
96
104
  if (kind === "asset" && u.protocol === ASSET_SCHEME) return true;
105
+ if (kind === "tree" && u.protocol === TREE_SCHEME) return true;
97
106
  }
98
107
  return false;
99
108
  }