Skip to content

Commit 9dbfece

Browse files
committed
Support for PathV2
1 parent 09a1594 commit 9dbfece

File tree

2 files changed

+397
-3
lines changed

2 files changed

+397
-3
lines changed

PathSplitter/1.1.2/PathSplitter.js

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
var API_Meta = API_Meta||{}; //eslint-disable-line no-var
2+
API_Meta.PathSplitter={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
3+
{try{throw new Error('');}catch(e){API_Meta.PathSplitter.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-3);}}
4+
API_Meta.PathSplitter.version = '1.1.2';
5+
/* globals PathMath */
6+
/**
7+
* This script provides a way for players and GMs to split paths by their
8+
* intersections with another splitting path.
9+
* This could especially be useful for when corrections need to be made to
10+
* paths used for dynamic lighting.
11+
*
12+
* Simply draw a polygonal path intersecting the path you want to split up.
13+
* Select the main path and then the splitting path.
14+
* Then with the main and splitting paths selected,
15+
* enter the command '!pathSplit'.
16+
* The original path will be divided into new paths separated at the points
17+
* where the splitting path intersected the original path.
18+
*
19+
* This script also works with paths that have been scaled and rotated.'
20+
*
21+
* Requires:
22+
* VectorMath
23+
* MatrixMath
24+
* PathMath
25+
*/
26+
27+
(() => {
28+
29+
const PATHSPLIT_CMD = '!pathSplit';
30+
const PATHJOIN_CMD = '!pathJoin';
31+
const PATHSPLIT_COLOR_CMD = '!pathSplitColor';
32+
const EPSILON = 0.001;
33+
34+
/**
35+
* A 3-tuple representing a point of intersection between two line segments.
36+
* The first element is a Vector representing the point of intersection in
37+
* 2D homogenous coordinates.
38+
* The second element is the parametric coefficient for the intersection
39+
* along the first segment.
40+
* The third element is the parametric coefficient for the intersection
41+
* along the second segment.
42+
* @typedef {Array} Intersection
43+
*/
44+
45+
/**
46+
* A vector used to define a homogeneous point or a direction.
47+
* @typedef {number[]} Vector
48+
*/
49+
50+
/**
51+
* A line segment defined by two homogenous 2D points.
52+
* @typedef {Vector[]} Segment
53+
*/
54+
55+
let isJumpgate = ()=>{
56+
if(['jumpgate'].includes(Campaign().get('_release'))) {
57+
isJumpgate = () => true;
58+
} else {
59+
isJumpgate = () => false;
60+
}
61+
return isJumpgate();
62+
};
63+
64+
65+
// Initialize the script's state if it hasn't already been initialized.
66+
state.PathSplitter = state.PathSplitter || {
67+
splitPathColor: '#ff00ff' // pink
68+
};
69+
70+
71+
function _getSplitSegmentPaths(mainSegments, splitSegments) {
72+
let resultSegPaths = [];
73+
let curPathSegs = [];
74+
75+
mainSegments.forEach( seg1 => {
76+
77+
// Find the points of intersection and their parametric coefficients.
78+
let intersections = [];
79+
splitSegments.forEach(seg2 => {
80+
let i = PathMath.segmentIntersection(seg1, seg2);
81+
if(i)
82+
intersections.push(i);
83+
});
84+
85+
if(intersections.length > 0) {
86+
// Sort the intersections in the order that they appear along seg1.
87+
intersections.sort((a, b) => {
88+
return a[1] - b[1];
89+
});
90+
91+
let lastPt = seg1[0];
92+
intersections.forEach( i => {
93+
// Complete the current segment path.
94+
curPathSegs.push([lastPt, i[0]]);
95+
resultSegPaths.push(curPathSegs);
96+
97+
// Start a new segment path.
98+
curPathSegs = [];
99+
lastPt = i[0];
100+
});
101+
curPathSegs.push([lastPt, seg1[1]]);
102+
}
103+
else {
104+
curPathSegs.push(seg1);
105+
}
106+
});
107+
resultSegPaths.push(curPathSegs);
108+
109+
return resultSegPaths;
110+
}
111+
112+
const ptDist = (p1,p2) => Math.sqrt(Math.pow(p2[0]-p1[0],2)+Math.pow(p2[1]-p1[1],2));
113+
114+
const reverseSegs = (segs) => [...segs.map(seg=>seg.reverse())].reverse();
115+
116+
function joinPaths(path1,path2){
117+
let p1Segments = PathMath.toSegments(path1);
118+
let p2Segments = PathMath.toSegments(path2);
119+
let p1Len = p1Segments.length;
120+
let p2Len = p2Segments.length;
121+
122+
let _pageid = path1.get('_pageid');
123+
let controlledby = path1.get('controlledby');
124+
let fill = path1.get('fill');
125+
let layer = path1.get('layer');
126+
let stroke = path1.get('stroke');
127+
let stroke_width = path1.get('stroke_width');
128+
let pathExtra = {
129+
_pageid,
130+
controlledby,
131+
fill,
132+
layer,
133+
stroke,
134+
stroke_width
135+
};
136+
137+
// $d({p1Segments,p1Len,p2Segments,p2Len});
138+
139+
let strategy = [
140+
{ st: 'ss', dist: ptDist(p1Segments[0][0],p2Segments[0][0]) },
141+
{ st: 'se', dist: ptDist(p1Segments[0][0],p2Segments[p2Len-1][1]) },
142+
{ st: 'es', dist: ptDist(p1Segments[p1Len-1][1],p2Segments[0][0]) },
143+
{ st: 'ee', dist: ptDist(p1Segments[p1Len-1][1],p2Segments[p2Len-1][1]) }
144+
].sort((a,b)=>a.dist-b.dist)[0];
145+
146+
switch(strategy.st){
147+
case 'es':
148+
break;
149+
case 'ss':
150+
p1Segments = reverseSegs(p1Segments);
151+
break;
152+
case 'se':
153+
p1Segments = reverseSegs(p1Segments);
154+
p2Segments = reverseSegs(p2Segments);
155+
break;
156+
case 'ee':
157+
p2Segments = reverseSegs(p2Segments);
158+
break;
159+
}
160+
161+
let segments;
162+
163+
if(strategy.dist>0){
164+
segments = [
165+
...p1Segments,
166+
[p1Segments[p1Len-1][1],p2Segments[0][0]],
167+
...p2Segments
168+
];
169+
} else {
170+
segments = [
171+
...p1Segments,
172+
...p2Segments
173+
];
174+
}
175+
176+
if(isJumpgate()){
177+
switch(path1.get('shape')){
178+
case 'free':
179+
pathExtra.shape='free'; // force back to freehand
180+
break;
181+
case 'pol':
182+
case 'eli':
183+
case 'rec':
184+
break;
185+
}
186+
}
187+
188+
let pathData = {
189+
...PathMath.segmentsToPath(segments),
190+
...pathExtra
191+
};
192+
let path = createObj(isJumpgate() ? 'pathv2' : 'path', pathData);
193+
if(path){
194+
path1.remove();
195+
path2.remove();
196+
}
197+
}
198+
199+
/**
200+
* Splits mainPath at its intersections with splitPath. The original path
201+
* is removed, being replaced by the new split up paths.
202+
* @param {Path} mainPath
203+
* @param {Path} splitPath
204+
* @return {Path[]}
205+
*/
206+
function splitPathAtIntersections(mainPath, splitPath) {
207+
let mainSegments = PathMath.toSegments(mainPath);
208+
let splitSegments = PathMath.toSegments(splitPath);
209+
let segmentPaths = _getSplitSegmentPaths(mainSegments, splitSegments);
210+
211+
// Convert the list of segment paths into paths.
212+
let _pageid = mainPath.get('_pageid');
213+
let controlledby = mainPath.get('controlledby');
214+
let fill = mainPath.get('fill');
215+
let layer = mainPath.get('layer');
216+
let stroke = mainPath.get('stroke');
217+
let stroke_width = mainPath.get('stroke_width');
218+
let pathExtra = {
219+
_pageid,
220+
controlledby,
221+
fill,
222+
layer,
223+
stroke,
224+
stroke_width
225+
};
226+
227+
const ptSame = (p1,p2)=> p1[0]===p2[0] && p1[1]===p2[1] && p1[2]===p2[2] ;
228+
229+
if(isJumpgate()){
230+
// remove zero length lines
231+
segmentPaths = segmentPaths
232+
.map(segs=> segs.filter(s => !ptSame(s[0],s[1])))
233+
.filter(segs => segs.length);
234+
235+
switch(mainPath.get('shape')){
236+
case 'free':
237+
pathExtra.shape='free'; // force back to freehand
238+
break;
239+
case 'pol':
240+
break;
241+
242+
case 'eli':
243+
case 'rec':
244+
// fix up endpoints
245+
if(segmentPaths.length > 1){
246+
let distCheck = PathMath.distanceToPoint(segmentPaths[0][0][0],splitPath);
247+
if(distCheck > EPSILON) {
248+
segmentPaths[0] = [
249+
...segmentPaths[segmentPaths.length-1],
250+
...segmentPaths[0]
251+
];
252+
delete segmentPaths[segmentPaths.length-1];
253+
segmentPaths = segmentPaths.filter(p=>null !== p && undefined !== p);
254+
}
255+
}
256+
break;
257+
258+
default:
259+
// pathv1 path, do nothing
260+
}
261+
}
262+
263+
let results = [];
264+
segmentPaths.forEach(segments => {
265+
let pathData = {
266+
...PathMath.segmentsToPath(segments),
267+
...pathExtra
268+
};
269+
let path = createObj(isJumpgate() ? 'pathv2' : 'path', pathData);
270+
results.push(path);
271+
});
272+
273+
// Remove the original path and the splitPath.
274+
mainPath.remove();
275+
splitPath.remove();
276+
277+
return results;
278+
}
279+
280+
on('ready', () => {
281+
let macro = findObjs({
282+
_type: 'macro',
283+
name: 'Pathsplitter'
284+
})[0];
285+
286+
if(!macro) {
287+
findObjs({
288+
_type: 'player'
289+
})
290+
.filter( player => playerIsGM(player.id))
291+
.forEach( gm => {
292+
createObj('macro', {
293+
_playerid: gm.get('_id'),
294+
name: 'Pathsplitter',
295+
action: PATHSPLIT_CMD
296+
});
297+
});
298+
}
299+
let macro2 = findObjs({
300+
_type: 'macro',
301+
name: 'Pathjoiner'
302+
})[0];
303+
304+
if(!macro2) {
305+
findObjs({
306+
_type: 'player'
307+
})
308+
.filter( player => playerIsGM(player.id))
309+
.forEach( gm => {
310+
createObj('macro', {
311+
_playerid: gm.get('_id'),
312+
name: 'Pathjoiner',
313+
action: PATHJOIN_CMD
314+
});
315+
});
316+
}
317+
});
318+
319+
on('chat:message', msg => {
320+
if(msg.type === 'api' && msg.content === PATHSPLIT_COLOR_CMD) {
321+
try {
322+
let selected = msg.selected;
323+
let path = findObjs({
324+
_type: 'path',
325+
_id: selected[0]._id
326+
})[0];
327+
328+
let stroke = path.get('stroke');
329+
state.PathSplitter.splitPathColor = stroke;
330+
}
331+
catch(err) {
332+
log('!pathSplit ERROR: ' + err.message);
333+
log(err.stack);
334+
}
335+
}
336+
else if(msg.type === 'api' && msg.content === PATHSPLIT_CMD) {
337+
try {
338+
let selected = msg.selected;
339+
if(!selected || selected.length !== 2 || ! /^path/.test(selected[0]._type) || ! /^path/.test(selected[1]._type) ) {
340+
let msg = `Two paths must be selected: the one you want to split, and the splitting path (color: <span style="background: ${state.PathSplitter.splitPathColor}; width: 16px; height: 16px; padding: 0.2em; font-weight: bold;">${state.PathSplitter.splitPathColor}</span>).`;
341+
sendChat('Pathsplitter', msg);
342+
throw new Error('Two paths must be selected.');
343+
}
344+
345+
let path1 = getObj(selected[0]._type,selected[0]._id);
346+
let path2 = getObj(selected[1]._type,selected[1]._id);
347+
348+
// Determine which path is the main path and which is the
349+
// splitting path.
350+
let mainPath, splitPath;
351+
if(path1.get('stroke') === state.PathSplitter.splitPathColor) {
352+
mainPath = path2;
353+
splitPath = path1;
354+
}
355+
else if(path2.get('stroke') === state.PathSplitter.splitPathColor) {
356+
mainPath = path1;
357+
splitPath = path2;
358+
}
359+
else {
360+
let msg = 'No splitting path selected. ';
361+
msg += `Current split color: <span style="background: ${state.PathSplitter.splitPathColor}; width: 16px; height: 16px; padding: 0.2em; font-weight: bold;">${state.PathSplitter.splitPathColor}</span>`;
362+
sendChat('Pathsplitter', msg);
363+
364+
throw new Error('No splitting path selected.');
365+
}
366+
splitPathAtIntersections(mainPath, splitPath);
367+
}
368+
catch(err) {
369+
log('!pathSplit ERROR: ' + err.message);
370+
log(err.stack);
371+
}
372+
}
373+
else if(msg.type === 'api' && msg.content === PATHJOIN_CMD) {
374+
try {
375+
let selected = msg.selected;
376+
if(!selected || selected.length !== 2 || ! /^path/.test(selected[0]._type) || ! /^path/.test(selected[1]._type) ) {
377+
let msg = `Two paths must be selected: the one you want to join.`;
378+
sendChat('Pathsplitter', msg);
379+
throw new Error('Two paths must be selected.');
380+
}
381+
382+
let path1 = getObj(selected[0]._type,selected[0]._id);
383+
let path2 = getObj(selected[1]._type,selected[1]._id);
384+
385+
joinPaths(path1,path2);
386+
}
387+
catch(err) {
388+
log('!pathSplit ERROR: ' + err.message);
389+
log(err.stack);
390+
}
391+
}
392+
});
393+
})();
394+
{try{throw new Error('');}catch(e){API_Meta.PathSplitter.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.PathSplitter.offset);}}

0 commit comments

Comments
 (0)