@popmelt.com/core 0.16.0 → 0.18.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
@@ -243,10 +243,10 @@ If the toolbar opens but cannot reach an AI, check that:
243
243
  - The framework plugin or config wrapper is installed
244
244
  - The development server was restarted after installation
245
245
 
246
- ## Image annotation
246
+ ## Media annotation
247
247
 
248
- Manual image annotation is enabled by default in `PopmeltProvider`, without a
249
- vision dependency or model download.
248
+ Manual image and video annotation are enabled by default in `PopmeltProvider`,
249
+ without a vision dependency or model download.
250
250
 
251
251
  ```tsx
252
252
  <PopmeltProvider>{children}</PopmeltProvider>
@@ -254,23 +254,31 @@ vision dependency or model download.
254
254
  // Optional: disable image annotation.
255
255
  <PopmeltProvider imageAnnotation={false}>{children}</PopmeltProvider>
256
256
 
257
+ // Optional: disable video annotation.
258
+ <PopmeltProvider videoAnnotation={false}>{children}</PopmeltProvider>
259
+
257
260
  // Optional: limit image discovery (all img elements are included by default).
258
261
  <PopmeltProvider imageAnnotation={{ selector: 'img[data-popmelt-media]' }}>
259
262
  {children}
260
263
  </PopmeltProvider>
261
264
  ```
262
265
 
263
- In Chat, drag over an image to create a named object. Double-click an object to
266
+ In Comment, drag over an image to create a named object. Double-click an object to
264
267
  rename or resize it, single-click it to comment, and right-click its label to
265
268
  delete it. Saved targets stay aligned as the image resizes.
266
269
 
270
+ The same interactions work on a paused video. A manual box stays at the saved
271
+ position unless a perception extension supplies motion tracking. Motion tracks
272
+ produced by an extension are persisted and remain replayable by Core alone.
273
+
267
274
  Image targets use browser-local storage by default. Supply
268
275
  `imageAnnotation.store` when the project needs another persistence layer. Add a
269
276
  stable `data-popmelt-media-id` to images whose URL may change or whose source
270
- appears more than once:
277
+ appears more than once. Use the same attribute for durable video identities:
271
278
 
272
279
  ```tsx
273
280
  <img data-popmelt-media-id="hero-product" src={imageUrl} alt="Product" />
281
+ <video data-popmelt-media-id="hero-reel" src={videoUrl} controls />
274
282
  ```
275
283
 
276
284
  No recognition or model download occurs unless See is installed and enabled.
@@ -278,8 +286,8 @@ No recognition or model download occurs unless See is installed and enabled.
278
286
  ## Add local object recognition with See
279
287
 
280
288
  [`@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.
289
+ browser-local object detection and optional video tracking to the existing media
290
+ annotation experience. It requires Core 0.17.0 or newer.
283
291
 
284
292
  Install it:
285
293
 
@@ -297,12 +305,12 @@ import { see } from '@popmelt.com/see';
297
305
  ```
298
306
 
299
307
  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
308
+ the same array. Model selection, self-hosting, recognition, and video tracking are
301
309
  documented in the See package README.
302
310
 
303
311
  ## Data and privacy
304
312
 
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.
313
+ Popmelt stores its annotations, screenshots, threads, UI preferences, and Imprint locally under `.popmelt/`. Unsent annotation drafts, preview state, and the default media-object stores use origin-scoped browser storage. Supply project-backed `imageAnnotation.store` and `videoAnnotation.store` implementations when media targets must survive browser-site-data removal or move between origins.
306
314
 
307
315
  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.
308
316
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "popmelt-core",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Ambient project memory and shared understanding for Codex",
5
5
  "author": {
6
6
  "name": "Popmelt",
@@ -1,5 +1,19 @@
1
1
  import { Camera, Object3D, Raycaster, WebGLRenderer } from 'three';
2
2
 
3
+ type AnnotationRevealStep = {
4
+ triggerSelector: string;
5
+ targetSelector?: string;
6
+ label: string;
7
+ };
8
+ type AnnotationRevealRecipe = {
9
+ steps: AnnotationRevealStep[];
10
+ location: string[];
11
+ };
12
+ type ReactSourceLocation = {
13
+ fileName: string;
14
+ lineNumber: number;
15
+ columnNumber?: number;
16
+ };
3
17
  /** Coordinates refer to the original image, not its responsive DOM rectangle. */
4
18
  type ImageObjectContext = {
5
19
  src: string;
@@ -19,6 +33,47 @@ type ImageObjectContext = {
19
33
  height: number;
20
34
  };
21
35
  };
36
+ /** Coordinates refer to the decoded video frame, not its responsive DOM box. */
37
+ type VideoObjectContext = {
38
+ src: string;
39
+ width: number;
40
+ height: number;
41
+ duration: number;
42
+ currentTime: number;
43
+ normalizedBox: {
44
+ x: number;
45
+ y: number;
46
+ width: number;
47
+ height: number;
48
+ };
49
+ box: {
50
+ x: number;
51
+ y: number;
52
+ width: number;
53
+ height: number;
54
+ };
55
+ trackStart: number;
56
+ trackEnd: number;
57
+ };
58
+ type ElementInfo = {
59
+ selector: string;
60
+ tagName: string;
61
+ /** Adapter-owned semantic name for a non-DOM subject. */
62
+ subjectLabel?: string;
63
+ id?: string;
64
+ className?: string;
65
+ textContent?: string;
66
+ dataAttributes?: Record<string, string>;
67
+ reactComponent?: string;
68
+ reactComponentPath?: string[];
69
+ reactSource?: ReactSourceLocation;
70
+ imageObject?: ImageObjectContext;
71
+ videoObject?: VideoObjectContext;
72
+ /** The real DOM surface, rather than the subject's hidden projection. */
73
+ sourceElement?: ElementInfo;
74
+ context?: string;
75
+ revealRecipe?: AnnotationRevealRecipe;
76
+ };
22
77
 
23
78
  type PopmeltSubjectBounds = {
24
79
  left: number;
@@ -26,6 +81,12 @@ type PopmeltSubjectBounds = {
26
81
  width: number;
27
82
  height: number;
28
83
  };
84
+ type PopmeltSubjectTrackingAnchor = {
85
+ /** Horizontal position normalized within the subject bounds. */
86
+ x: number;
87
+ /** Vertical position normalized within the subject bounds. */
88
+ y: number;
89
+ };
29
90
  type PopmeltSubject = {
30
91
  key: string;
31
92
  label: string;
@@ -37,6 +98,9 @@ type PopmeltSubject = {
37
98
  /** Live owning surface; never serialized or included in model context. */
38
99
  sourceElement?: Element;
39
100
  imageObject?: ImageObjectContext;
101
+ videoObject?: VideoObjectContext;
102
+ /** Optional provider-neutral foreground anchors used by motion tracking. */
103
+ trackingAnchors?: readonly PopmeltSubjectTrackingAnchor[];
40
104
  };
41
105
  type PopmeltSurfaceSnapshot = {
42
106
  element: HTMLCanvasElement;
@@ -58,12 +122,16 @@ type PopmeltSubjectCreationSession = {
58
122
  type PopmeltSubjectAdapter = {
59
123
  id: string;
60
124
  /** Surface ownership; an explicit image adapter replaces Core's default. */
61
- surfaceKind?: 'image';
125
+ surfaceKind?: 'image' | 'video';
126
+ /** Resolve once the adapter's persisted subject index is available. */
127
+ whenReady?(): Promise<void>;
62
128
  hitTest(clientPoint: {
63
129
  x: number;
64
130
  y: number;
65
131
  }): PopmeltSubject | null;
66
132
  resolve(key: string): PopmeltSubject | null;
133
+ /** Recreate a missing durable subject from annotation-captured context. */
134
+ restoreSubject?(key: string, elementInfo: ElementInfo): PopmeltSubject | null;
67
135
  /** Optional clean subject crop, without Popmelt's overlay. */
68
136
  captureSubject?(key: string): Blob | null | Promise<Blob | null>;
69
137
  /** Optional bitmap capture for renderer surfaces that DOM cloning cannot read. */
@@ -77,6 +145,8 @@ type PopmeltSubjectAdapter = {
77
145
  renameSubject?(key: string, label: string): PopmeltSubject | null;
78
146
  /** Replace a subject's viewport bounds, projected back into adapter space. */
79
147
  updateSubjectBounds?(key: string, bounds: PopmeltSubjectBounds): PopmeltSubject | null;
148
+ /** Replace optional foreground anchors normalized within subject bounds. */
149
+ updateSubjectTrackingAnchors?(key: string, anchors: readonly PopmeltSubjectTrackingAnchor[]): PopmeltSubject | null;
80
150
  /** Remove a user-created subject or hide an adapter-owned subject. */
81
151
  removeSubject?(key: string): void;
82
152
  };
@@ -1 +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};
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)),u=Math.min(r.right,Math.max(...t)),m=Math.max(r.top,Math.min(...a)),o=Math.min(r.bottom,Math.max(...a));return u<=i||o<=m?null:{left:i,top:m,width:u-i,height:o-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 u=P(t,e.camera,e.canvas);return u?{key:i,label:e.label?.(t)??T(t),sourceElement:e.canvas,bounds:u,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 u of n.intersectObject(e.scene,!0)){let m=e.select?e.select(u.object):R(u.object);if(!m||!m.visible)continue;let o=r(m,u.object);if(o)return o}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};