@popmelt.com/core 0.14.2 → 0.16.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 CHANGED
@@ -29,6 +29,13 @@ From inside your project, give your coding agent this prompt:
29
29
 
30
30
  Your agent can detect the framework, add the provider and integration, and preserve your existing development command. Restart the development server when it is finished.
31
31
 
32
+ To add local object recognition to an existing Popmelt project, use:
33
+
34
+ > Add `@popmelt.com/see` to this Popmelt Core project
35
+
36
+ The integration installs See and adds it to the existing Core provider's
37
+ `extensions` array without replacing Core.
38
+
32
39
  ## Use Popmelt
33
40
 
34
41
  1. Run your project locally and open it in a browser
@@ -50,13 +57,13 @@ Double-click visible text to edit it directly. Popmelt records the text change s
50
57
 
51
58
  Steer previews code-native design changes directly on the page. Point to an element, choose a family, narrow to a property, and choose a value. Click the target or press Enter at the value level to stage the preview.
52
59
 
53
- | Keys | Family |
54
- | --- | --- |
55
- | `P` / `M` | Padding / margin |
56
- | `B` / `R` | Borders / radii |
57
- | `F` / `G` | Flexbox / grid |
58
- | `FO` / `BG` | Type / background color |
59
- | `T` / `O` | Transform / opacity and blend mode |
60
+ | Keys | Family |
61
+ | ----------- | ---------------------------------- |
62
+ | `P` / `M` | Padding / margin |
63
+ | `B` / `R` | Borders / radii |
64
+ | `F` / `G` | Flexbox / grid |
65
+ | `FO` / `BG` | Type / background color |
66
+ | `T` / `O` | Transform / opacity and blend mode |
60
67
 
61
68
  The omnidial can be driven entirely from the keyboard: `Q` and `E` move between properties, `A` and `D` or the arrow keys move between values, `W` or Enter goes deeper or applies, and `S` or Escape goes back. Press Space to search families, properties, colors, and values by name.
62
69
 
@@ -79,15 +86,18 @@ Archiving hides a thread from the active canvas without deleting its stored conv
79
86
 
80
87
  ### Pointer gestures
81
88
 
82
- | Gesture | Action |
83
- | --- | --- |
84
- | Shake the pointer quickly | Switch between Chat and Steer |
85
- | Hold Command/Control while pointing | Pass through the default target to the deepest element under the pointer |
86
- | Double-click text in Chat | Edit the text directly |
87
- | Click the toolbar annotation counter | Cycle through annotation bundles, including other routes |
88
- | Scroll the annotation counter | Change the active annotation color |
89
- | Hold the annotation counter | Reset the active color |
90
- | Right-click Popmelt's canvas, then right-click again | Pass through to the page's native context menu |
89
+ | Gesture | Action |
90
+ | -------------------------------------------------- | ------------------------------------------------------------------------ |
91
+ | Shake the pointer quickly | Switch between Chat and Steer |
92
+ | Hold Command/Control while pointing | Pass through the default target to the deepest element under the pointer |
93
+ | Double-click text in Chat | Edit the text directly |
94
+ | Click the toolbar annotation counter | Cycle through annotation bundles, including other routes |
95
+ | Scroll the annotation counter | Change the active annotation color |
96
+ | Hold the annotation counter | Reset the active color |
97
+ | Right-click Popmelt's canvas | Enter persistent page passthrough while keeping Popmelt overlays visible |
98
+ | Drag, scroll, pinch, or right-click in passthrough | Interact with the page and open its native context menu |
99
+ | Click a valid target in passthrough | Apply the active Comment or Steer action without leaving passthrough |
100
+ | Press Escape or choose another tool | Exit passthrough |
91
101
 
92
102
  ## Ambient Popmelt in Codex
93
103
 
@@ -233,9 +243,66 @@ If the toolbar opens but cannot reach an AI, check that:
233
243
  - The framework plugin or config wrapper is installed
234
244
  - The development server was restarted after installation
235
245
 
246
+ ## Image annotation
247
+
248
+ Manual image annotation is enabled by default in `PopmeltProvider`, without a
249
+ vision dependency or model download.
250
+
251
+ ```tsx
252
+ <PopmeltProvider>{children}</PopmeltProvider>
253
+
254
+ // Optional: disable image annotation.
255
+ <PopmeltProvider imageAnnotation={false}>{children}</PopmeltProvider>
256
+
257
+ // Optional: limit image discovery (all img elements are included by default).
258
+ <PopmeltProvider imageAnnotation={{ selector: 'img[data-popmelt-media]' }}>
259
+ {children}
260
+ </PopmeltProvider>
261
+ ```
262
+
263
+ In Chat, drag over an image to create a named object. Double-click an object to
264
+ rename or resize it, single-click it to comment, and right-click its label to
265
+ delete it. Saved targets stay aligned as the image resizes.
266
+
267
+ Image targets use browser-local storage by default. Supply
268
+ `imageAnnotation.store` when the project needs another persistence layer. Add a
269
+ stable `data-popmelt-media-id` to images whose URL may change or whose source
270
+ appears more than once:
271
+
272
+ ```tsx
273
+ <img data-popmelt-media-id="hero-product" src={imageUrl} alt="Product" />
274
+ ```
275
+
276
+ No recognition or model download occurs unless See is installed and enabled.
277
+
278
+ ## Add local object recognition with See
279
+
280
+ [`@popmelt.com/see`](https://www.npmjs.com/package/@popmelt.com/see) adds
281
+ browser-local object detection to the existing image-annotation experience. It
282
+ requires Core 0.16.0 or newer.
283
+
284
+ Install it:
285
+
286
+ ```bash
287
+ npm install @popmelt.com/see
288
+ ```
289
+
290
+ Then add the extension to the existing Core provider:
291
+
292
+ ```tsx
293
+ import { PopmeltProvider } from '@popmelt.com/core';
294
+ import { see } from '@popmelt.com/see';
295
+
296
+ <PopmeltProvider extensions={[see]}>{children}</PopmeltProvider>;
297
+ ```
298
+
299
+ Recognition loads only when it is needed. Other extensions can be included in
300
+ the same array. Model selection, self-hosting, and recognition settings are
301
+ documented in the See package README.
302
+
236
303
  ## Data and privacy
237
304
 
238
- Popmelt stores its annotations, screenshots, threads, UI preferences, and Imprint locally under `.popmelt/`. Unsent annotation drafts and preview state also use browser storage.
305
+ Popmelt stores its annotations, screenshots, threads, UI preferences, and Imprint locally under `.popmelt/`. Unsent annotation drafts, preview state, and the default image-object store use origin-scoped browser storage. Supply a project-backed `imageAnnotation.store` when image targets must survive browser-site-data removal or move between origins.
239
306
 
240
307
  When you send a thread, the context needed to complete that work is passed to your selected AI provider and is subject to that provider's privacy policy.
241
308
 
@@ -243,19 +310,19 @@ Popmelt is designed for local development. As with any AI-assisted coding, your
243
310
 
244
311
  ## Useful shortcuts
245
312
 
246
- | Shortcut | Action |
247
- | --- | --- |
248
- | Double-tap Command or Control | Open or collapse Popmelt |
249
- | `C` | Enter Chat mode |
250
- | `S` | Enter Steer mode |
251
- | `I` | Enter Imprint mode |
252
- | Command/Control+Enter | Send all pending work |
253
- | Command/Control+C | Capture a screenshot for pending work when no text is selected |
254
- | Command/Control+Z | Undo a pending change |
255
- | Command/Control+Shift+Z | Redo a pending change |
256
- | Backspace/Delete | Archive the open or selected historical thread |
257
- | Command/Control+Backspace/Delete | Clear all annotations and pending changes |
258
- | Escape | Go back, cancel, close, discard, or deselect in the current context |
313
+ | Shortcut | Action |
314
+ | -------------------------------- | ------------------------------------------------------------------- |
315
+ | Double-tap Command or Control | Open or collapse Popmelt |
316
+ | `C` | Enter Chat mode |
317
+ | `S` | Enter Steer mode |
318
+ | `I` | Enter Imprint mode |
319
+ | Command/Control+Enter | Send all pending work |
320
+ | Command/Control+C | Capture a screenshot for pending work when no text is selected |
321
+ | Command/Control+Z | Undo a pending change |
322
+ | Command/Control+Shift+Z | Redo a pending change |
323
+ | Backspace/Delete | Archive the open or selected historical thread |
324
+ | Command/Control+Backspace/Delete | Clear all annotations and pending changes |
325
+ | Escape | Go back, cancel, close, discard, or deselect in the current context |
259
326
 
260
327
  Steer additionally owns the unmodified letter, arrow, Enter, Escape, and Space keys described above while it is active. Popmelt leaves these shortcuts alone while you are typing in an input, textarea, or editable text surface.
261
328
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "popmelt-core",
3
- "version": "0.14.2",
3
+ "version": "0.16.0",
4
4
  "description": "Ambient project memory and shared understanding for Codex",
5
5
  "author": {
6
6
  "name": "Popmelt",
@@ -0,0 +1,115 @@
1
+ import { Camera, Object3D, Raycaster, WebGLRenderer } from 'three';
2
+
3
+ /** Coordinates refer to the original image, not its responsive DOM rectangle. */
4
+ type ImageObjectContext = {
5
+ src: string;
6
+ alt: string;
7
+ width: number;
8
+ height: number;
9
+ box: {
10
+ x: number;
11
+ y: number;
12
+ width: number;
13
+ height: number;
14
+ };
15
+ normalizedBox: {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ };
21
+ };
22
+
23
+ type PopmeltSubjectBounds = {
24
+ left: number;
25
+ top: number;
26
+ width: number;
27
+ height: number;
28
+ };
29
+ type PopmeltSubject = {
30
+ key: string;
31
+ label: string;
32
+ bounds: PopmeltSubjectBounds;
33
+ /** Viewport bounds of the renderer surface that owns this subject. */
34
+ surfaceBounds?: PopmeltSubjectBounds;
35
+ context?: string;
36
+ data?: Record<string, string>;
37
+ /** Live owning surface; never serialized or included in model context. */
38
+ sourceElement?: Element;
39
+ imageObject?: ImageObjectContext;
40
+ };
41
+ type PopmeltSurfaceSnapshot = {
42
+ element: HTMLCanvasElement;
43
+ dataUrl: string;
44
+ };
45
+ type PopmeltSubjectCreationSession = {
46
+ key: string;
47
+ update(clientPoint: {
48
+ x: number;
49
+ y: number;
50
+ }): PopmeltSubject | null;
51
+ commit(): PopmeltSubject | null;
52
+ cancel(): void;
53
+ };
54
+ /**
55
+ * Minimal renderer boundary for non-DOM subjects. The adapter owns hit testing,
56
+ * durable keys, and viewport projection; Popmelt owns annotations and threads.
57
+ */
58
+ type PopmeltSubjectAdapter = {
59
+ id: string;
60
+ /** Surface ownership; an explicit image adapter replaces Core's default. */
61
+ surfaceKind?: 'image';
62
+ hitTest(clientPoint: {
63
+ x: number;
64
+ y: number;
65
+ }): PopmeltSubject | null;
66
+ resolve(key: string): PopmeltSubject | null;
67
+ /** Optional clean subject crop, without Popmelt's overlay. */
68
+ captureSubject?(key: string): Blob | null | Promise<Blob | null>;
69
+ /** Optional bitmap capture for renderer surfaces that DOM cloning cannot read. */
70
+ captureSurfaces?(): readonly PopmeltSurfaceSnapshot[] | Promise<readonly PopmeltSurfaceSnapshot[]>;
71
+ /** Begin a context-aware subject-creation gesture at a viewport point. */
72
+ beginCreate?(clientPoint: {
73
+ x: number;
74
+ y: number;
75
+ }): PopmeltSubjectCreationSession | null;
76
+ /** Rename a subject without changing its durable key. */
77
+ renameSubject?(key: string, label: string): PopmeltSubject | null;
78
+ /** Replace a subject's viewport bounds, projected back into adapter space. */
79
+ updateSubjectBounds?(key: string, bounds: PopmeltSubjectBounds): PopmeltSubject | null;
80
+ /** Remove a user-created subject or hide an adapter-owned subject. */
81
+ removeSubject?(key: string): void;
82
+ };
83
+
84
+ type ThreeSubjectAdapterOptions = {
85
+ canvas: HTMLCanvasElement;
86
+ camera: Camera;
87
+ scene: Object3D;
88
+ raycaster?: Raycaster;
89
+ /** Select a meaningful parent from the raw intersected render object. */
90
+ select?: (object: Object3D) => Object3D | null;
91
+ /** Override durable identity. Explicit application IDs are preferred. */
92
+ key?: (object: Object3D) => string | null | undefined;
93
+ label?: (object: Object3D) => string;
94
+ /** Add application-specific provenance or source hints to the model context. */
95
+ describe?: (object: Object3D) => Record<string, string | undefined>;
96
+ /** Capture the renderer's current frame for DOM-cloned screenshots. */
97
+ captureFrame?: () => string | null | Promise<string | null>;
98
+ };
99
+ /**
100
+ * Read the renderer's visible frame without requiring preserveDrawingBuffer.
101
+ * Rendering and reading synchronously preserves the application's real output
102
+ * pipeline while the previous render target and XR state are restored afterward.
103
+ */
104
+ declare function captureThreeRendererFrame(renderer: WebGLRenderer, scene: Object3D, camera: Camera): string | null;
105
+ /** Stable while the scene's semantic hierarchy remains stable across reloads. */
106
+ declare function threeObjectPath(object: Object3D): string;
107
+ /**
108
+ * Collapses glTF material primitives such as `Object649_normal_0` onto their
109
+ * named parent `Object649`. Apps opt into this policy through `select` so raw
110
+ * mesh targeting remains available when material-level annotation is useful.
111
+ */
112
+ declare function selectThreeNamedPrimitiveParent(object: Object3D): Object3D;
113
+ declare function createThreeSubjectAdapter(options: ThreeSubjectAdapterOptions): PopmeltSubjectAdapter;
114
+
115
+ export { type ThreeSubjectAdapterOptions, captureThreeRendererFrame, createThreeSubjectAdapter, selectThreeNamedPrimitiveParent, threeObjectPath };
@@ -0,0 +1 @@
1
+ import{Box3 as y,Raycaster as x,Vector2 as O,Vector3 as j}from"three";function b(e){return[e.x,e.y,e.z].map(n=>Number(n.toFixed(4))).join(", ")}function h(e){let n=e.material;return n&&(Array.isArray(n)?n:[n]).map(r=>r.name?.trim()||r.type).filter(Boolean).join(", ")||void 0}function M(e,n,c){let s=e.domElement.width,r=e.domElement.height;if(s<=0||r<=0)return null;let l=e.getRenderTarget(),t=e.xr.enabled;try{return e.xr.enabled=!1,e.setRenderTarget(null),e.render(n,c),e.domElement.toDataURL("image/png")}catch{return null}finally{e.setRenderTarget(l),e.xr.enabled=t}}function D(e){let s=(e.parent?.children??[]).filter(t=>t.type===e.type&&t.name===e.name),r=Math.max(0,s.indexOf(e)),l=e.name.trim()||e.type;return`${encodeURIComponent(l)}[${r}]`}function f(e){let n=[],c=e;for(;c?.parent;)n.push(D(c)),c=c.parent;return n.reverse().join("/")}function v(e){let n=e.userData.popmeltId;return typeof n=="string"&&n.trim()?n.trim():f(e)}function T(e){let n=e.userData.popmeltLabel;return typeof n=="string"&&n.trim()?n.trim():e.name.trim()||e.type}function R(e){let n=e;for(;n.parent;){let c=n.name.trim(),s=c.match(/^(.*)_(?:alpha|normal|material)(?:_\d+)+$/i),r=n.parent;for(;r.parent&&!r.name.trim();)r=r.parent;let l=r.name.trim();if(!l||!c.startsWith(`${l}_`)&&!s)break;n=r}return n}function P(e,n,c){let s=new y().setFromObject(e,!0);if(s.isEmpty())return null;let r=c.getBoundingClientRect(),l=[];for(let p of[s.min.x,s.max.x])for(let d of[s.min.y,s.max.y])for(let g of[s.min.z,s.max.z])l.push(new j(p,d,g).project(n));let t=l.map(p=>r.left+(p.x+1)/2*r.width),a=l.map(p=>r.top+(1-p.y)/2*r.height),i=Math.max(r.left,Math.min(...t)),o=Math.min(r.right,Math.max(...t)),m=Math.max(r.top,Math.min(...a)),u=Math.min(r.bottom,Math.max(...a));return o<=i||u<=m?null:{left:i,top:m,width:o-i,height:u-m}}function C(e){let n=e.raycaster??new x,c=new O,s=t=>e.key?.(t)??v(t),r=(t,a)=>{let i=s(t);if(!i)return null;let o=P(t,e.camera,e.canvas);return o?{key:i,label:e.label?.(t)??T(t),bounds:o,context:f(t),data:{renderer:"Three.js",objectType:t.type,...t.name?{objectName:t.name}:{},scenePath:f(t),...a&&a!==t?{renderObjectName:a.name.trim()||a.type,renderObjectType:a.type,renderScenePath:f(a)}:{},position:b(t.position),rotation:b(t.rotation),scale:b(t.scale),visible:String(t.visible),...h(t)?{material:h(t)}:{},...Object.fromEntries(Object.entries(e.describe?.(t)??{}).filter(m=>typeof m[1]=="string"&&m[1].length>0))}}:null},l=t=>{let a=null;return e.scene.traverse(i=>{!a&&s(i)===t&&(a=i)}),a};return{id:"three",hitTest({x:t,y:a}){let i=e.canvas.getBoundingClientRect();if(t<i.left||t>i.right||a<i.top||a>i.bottom)return null;c.set((t-i.left)/i.width*2-1,-((a-i.top)/i.height)*2+1),n.setFromCamera(c,e.camera);for(let o of n.intersectObject(e.scene,!0)){let m=e.select?e.select(o.object):R(o.object);if(!m||!m.visible)continue;let u=r(m,o.object);if(u)return u}return null},resolve(t){let a=l(t);return a?r(a):null},...e.captureFrame?{async captureSurfaces(){let t=await e.captureFrame?.();return t?[{element:e.canvas,dataUrl:t}]:[]}}:{}}}export{M as captureThreeRendererFrame,C as createThreeSubjectAdapter,R as selectThreeNamedPrimitiveParent,f as threeObjectPath};