@@ -173,6 +173,7 @@ export class CustomDrawing extends Drawing {
173173 */
174174 _onClickLeft2 ( event ) {
175175 const noteData = this . document . flags ?. [ MODULE_ID ] ;
176+ if ( noteData ?. type === "pin" ) return ;
176177 if ( noteData ?. type ) {
177178 // Open the detail view previewer
178179 new NotePreviewer ( this . document ) . render ( true ) ;
@@ -230,6 +231,102 @@ export class CustomDrawing extends Drawing {
230231 menu . style . left = `${ x } px` ;
231232 menu . style . zIndex = '10000' ;
232233
234+ if ( noteData . type === "pin" ) {
235+ const editOption = document . createElement ( 'div' ) ;
236+ editOption . innerHTML = '<i class="fas fa-edit"></i> Edit' ;
237+ editOption . classList . add ( 'ib-context-menu-item' ) ;
238+ editOption . onclick = ( e ) => {
239+ e . stopPropagation ( ) ;
240+ this . document . sheet . render ( true ) ;
241+ menu . remove ( ) ;
242+ } ;
243+
244+ if ( noteData . linkedObject ) {
245+ const linkMatch = noteData . linkedObject . match ( / \[ ( [ ^ \] ] + ) \] (?: \{ ( [ ^ \} ] + ) \} ) ? / ) ;
246+ if ( linkMatch ) {
247+ const uuid = linkMatch [ 1 ] ;
248+ const name = linkMatch [ 2 ] || "Linked Object" ;
249+ const linkOption = document . createElement ( 'div' ) ;
250+ linkOption . innerHTML = `<i class="fas fa-link"></i> Open: ${ name } ` ;
251+ linkOption . classList . add ( 'ib-context-menu-item' ) ;
252+ linkOption . onclick = async ( e ) => {
253+ e . stopPropagation ( ) ;
254+ menu . remove ( ) ;
255+ try {
256+ const doc = await fromUuid ( uuid ) ;
257+ if ( doc ) {
258+ if ( doc . testUserPermission ( game . user , "LIMITED" ) ) {
259+ doc . sheet . render ( true ) ;
260+ } else {
261+ ui . notifications . warn ( `You do not have permission to view ${ doc . name } .` ) ;
262+ }
263+ } else {
264+ ui . notifications . warn ( `Could not find linked document.` ) ;
265+ }
266+ } catch ( err ) {
267+ console . error ( "Investigation Board: Error opening linked document from menu" , err ) ;
268+ }
269+ } ;
270+ menu . appendChild ( linkOption ) ;
271+ }
272+ }
273+
274+ const removeConnectionsOption = document . createElement ( 'div' ) ;
275+ removeConnectionsOption . innerHTML = '<i class="fas fa-cut"></i> Remove Connections' ;
276+ removeConnectionsOption . classList . add ( 'ib-context-menu-item' ) ;
277+ removeConnectionsOption . onclick = async ( e ) => {
278+ e . stopPropagation ( ) ;
279+ menu . remove ( ) ;
280+ const confirm = await foundry . applications . api . DialogV2 . confirm ( {
281+ window : { title : "Remove All Connections" } ,
282+ content : `<p>Are you sure you want to remove ALL yarn connections connected to this note?</p>` ,
283+ rejectClose : false ,
284+ modal : true
285+ } ) ;
286+ if ( confirm ) {
287+ const noteId = this . document . id ;
288+ await collaborativeUpdate ( noteId , { [ `flags.${ MODULE_ID } .connections` ] : [ ] } ) ;
289+ const otherNotesWithConnections = canvas . drawings . placeables . filter ( d => {
290+ if ( d . document . id === noteId ) return false ;
291+ const conns = d . document . flags [ MODULE_ID ] ?. connections ;
292+ return conns && conns . some ( c => c . targetId === noteId ) ;
293+ } ) ;
294+ for ( let otherNote of otherNotesWithConnections ) {
295+ const currentConns = otherNote . document . flags [ MODULE_ID ] . connections ;
296+ const updatedConns = currentConns . filter ( c => c . targetId !== noteId ) ;
297+ await collaborativeUpdate ( otherNote . document . id , { [ `flags.${ MODULE_ID } .connections` ] : updatedConns } ) ;
298+ }
299+ drawAllConnectionLines ( ) ;
300+ ui . notifications . info ( "All related connections removed." ) ;
301+ }
302+ } ;
303+
304+ const deleteOption = document . createElement ( 'div' ) ;
305+ deleteOption . innerHTML = '<i class="fas fa-trash"></i> Delete' ;
306+ deleteOption . classList . add ( 'ib-context-menu-item' ) ;
307+ deleteOption . onclick = async ( e ) => {
308+ e . stopPropagation ( ) ;
309+ menu . remove ( ) ;
310+ const confirm = await foundry . applications . api . DialogV2 . confirm ( {
311+ window : { title : "Delete Pin" } ,
312+ content : `<p>Are you sure you want to delete this pin?</p>` ,
313+ rejectClose : false ,
314+ modal : true
315+ } ) ;
316+ if ( confirm ) {
317+ await collaborativeDelete ( this . document . id ) ;
318+ }
319+ } ;
320+
321+ menu . appendChild ( editOption ) ;
322+ menu . appendChild ( removeConnectionsOption ) ;
323+ menu . appendChild ( deleteOption ) ;
324+ document . body . appendChild ( menu ) ;
325+ const closeMenu = ( e ) => { if ( ! menu . contains ( e . target ) ) { menu . remove ( ) ; document . removeEventListener ( 'mousedown' , closeMenu ) ; window . removeEventListener ( 'wheel' , closeMenu ) ; } } ;
326+ setTimeout ( ( ) => { document . addEventListener ( 'mousedown' , closeMenu ) ; window . addEventListener ( 'wheel' , closeMenu ) ; } , 100 ) ;
327+ return ;
328+ }
329+
233330 const editOption = document . createElement ( 'div' ) ;
234331 editOption . innerHTML = '<i class="fas fa-edit"></i> Edit' ;
235332 editOption . classList . add ( 'ib-context-menu-item' ) ;
@@ -585,6 +682,65 @@ export class CustomDrawing extends Drawing {
585682 return ; // Early exit for media notes
586683 }
587684
685+ // PIN ONLY LAYOUT
686+ if ( noteData . type === "pin" ) {
687+ const width = this . document . shape . width || 50 ;
688+ const height = this . document . shape . height || 50 ;
689+
690+ // No background for pin-only
691+ if ( this . bgSprite ) {
692+ this . removeChild ( this . bgSprite ) ;
693+ this . bgSprite . destroy ( ) ;
694+ this . bgSprite = null ;
695+ }
696+ if ( this . bgShadow ) {
697+ this . removeChild ( this . bgShadow ) ;
698+ this . bgShadow . destroy ( ) ;
699+ this . bgShadow = null ;
700+ }
701+ if ( this . photoImageSprite ) {
702+ this . removeChild ( this . photoImageSprite ) ;
703+ this . photoImageSprite . destroy ( ) ;
704+ this . photoImageSprite = null ;
705+ }
706+
707+ // --- Pin Sprite ---
708+ const pinSetting = game . settings . get ( MODULE_ID , "pinColor" ) ;
709+ if ( ! this . pinSprite || ! this . pinSprite . parent ) {
710+ if ( this . pinSprite ) this . pinSprite . destroy ( ) ;
711+ this . pinSprite = new PIXI . Sprite ( ) ;
712+ this . addChild ( this . pinSprite ) ;
713+ }
714+
715+ let pinColor = noteData . pinColor ;
716+ if ( ! pinColor ) {
717+ pinColor = ( pinSetting === "random" )
718+ ? PIN_COLORS [ Math . floor ( Math . random ( ) * PIN_COLORS . length ) ]
719+ : ( pinSetting === "none" ? "redPin.webp" : `${ pinSetting } Pin.webp` ) ;
720+ await collaborativeUpdate ( this . document . id , { [ `flags.${ MODULE_ID } .pinColor` ] : pinColor } ) ;
721+ }
722+
723+ const pinImage = `modules/investigation-board/assets/${ pinColor } ` ;
724+ try {
725+ const texture = await PIXI . Assets . load ( pinImage ) ;
726+ if ( texture && this . pinSprite && this . pinSprite . parent ) {
727+ this . pinSprite . texture = texture ;
728+ this . pinSprite . width = width ;
729+ this . pinSprite . height = height ;
730+ this . pinSprite . position . set ( 0 , 0 ) ;
731+ }
732+ } catch ( err ) {
733+ console . error ( `Failed to load pin texture: ${ pinImage } ` , err ) ;
734+ }
735+
736+ // Hide text
737+ if ( this . noteText ) this . noteText . visible = false ;
738+ if ( this . identityNameText ) this . identityNameText . visible = false ;
739+ if ( this . futuristicText ) this . futuristicText . visible = false ;
740+
741+ return ;
742+ }
743+
588744 // HANDOUT NOTE LAYOUT (Image-only, transparent background)
589745 if ( isHandout ) {
590746 const drawingWidth = this . document . shape . width || 400 ;
@@ -1139,6 +1295,16 @@ export class CustomDrawing extends Drawing {
11391295 y : this . document . y + 23
11401296 } ;
11411297 }
1298+
1299+ // Pin-only notes
1300+ if ( noteData . type === "pin" ) {
1301+ const width = this . document . shape . width || 50 ;
1302+ const height = this . document . shape . height || 50 ;
1303+ return {
1304+ x : this . document . x + width / 2 ,
1305+ y : this . document . y + height / 2
1306+ } ;
1307+ }
11421308
11431309 const isPhoto = noteData . type === "photo" ;
11441310 const isIndex = noteData . type === "index" ;
0 commit comments