@fieldnotes/core 0.7.0 → 0.8.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/dist/index.cjs +285 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +284 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -60,6 +60,7 @@ __export(index_exports, {
|
|
|
60
60
|
createShape: () => createShape,
|
|
61
61
|
createStroke: () => createStroke,
|
|
62
62
|
createText: () => createText,
|
|
63
|
+
exportImage: () => exportImage,
|
|
63
64
|
exportState: () => exportState,
|
|
64
65
|
findBindTarget: () => findBindTarget,
|
|
65
66
|
findBoundArrows: () => findBoundArrows,
|
|
@@ -1769,7 +1770,7 @@ function createImage(input) {
|
|
|
1769
1770
|
};
|
|
1770
1771
|
}
|
|
1771
1772
|
function createHtmlElement(input) {
|
|
1772
|
-
|
|
1773
|
+
const el = {
|
|
1773
1774
|
id: createId("html"),
|
|
1774
1775
|
type: "html",
|
|
1775
1776
|
position: input.position,
|
|
@@ -1778,6 +1779,8 @@ function createHtmlElement(input) {
|
|
|
1778
1779
|
layerId: input.layerId ?? "",
|
|
1779
1780
|
size: input.size
|
|
1780
1781
|
};
|
|
1782
|
+
if (input.domId) el.domId = input.domId;
|
|
1783
|
+
return el;
|
|
1781
1784
|
}
|
|
1782
1785
|
function createShape(input) {
|
|
1783
1786
|
return {
|
|
@@ -1826,6 +1829,256 @@ function createText(input) {
|
|
|
1826
1829
|
};
|
|
1827
1830
|
}
|
|
1828
1831
|
|
|
1832
|
+
// src/canvas/export-image.ts
|
|
1833
|
+
function getStrokeBounds(el) {
|
|
1834
|
+
if (el.type !== "stroke") return null;
|
|
1835
|
+
if (el.points.length === 0) return null;
|
|
1836
|
+
let minX = Infinity;
|
|
1837
|
+
let minY = Infinity;
|
|
1838
|
+
let maxX = -Infinity;
|
|
1839
|
+
let maxY = -Infinity;
|
|
1840
|
+
for (const p of el.points) {
|
|
1841
|
+
const px = el.position.x + p.x;
|
|
1842
|
+
const py = el.position.y + p.y;
|
|
1843
|
+
minX = Math.min(minX, px);
|
|
1844
|
+
minY = Math.min(minY, py);
|
|
1845
|
+
maxX = Math.max(maxX, px);
|
|
1846
|
+
maxY = Math.max(maxY, py);
|
|
1847
|
+
}
|
|
1848
|
+
const pad = el.width / 2;
|
|
1849
|
+
return {
|
|
1850
|
+
x: minX - pad,
|
|
1851
|
+
y: minY - pad,
|
|
1852
|
+
w: maxX - minX + el.width,
|
|
1853
|
+
h: maxY - minY + el.width
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
function getElementRect(el) {
|
|
1857
|
+
switch (el.type) {
|
|
1858
|
+
case "stroke":
|
|
1859
|
+
return getStrokeBounds(el);
|
|
1860
|
+
case "arrow": {
|
|
1861
|
+
const b = getArrowBounds(el.from, el.to, el.bend);
|
|
1862
|
+
const pad = el.width / 2 + 14;
|
|
1863
|
+
return { x: b.x - pad, y: b.y - pad, w: b.w + pad * 2, h: b.h + pad * 2 };
|
|
1864
|
+
}
|
|
1865
|
+
case "grid":
|
|
1866
|
+
return null;
|
|
1867
|
+
case "note":
|
|
1868
|
+
case "image":
|
|
1869
|
+
case "html":
|
|
1870
|
+
case "text":
|
|
1871
|
+
case "shape":
|
|
1872
|
+
if ("size" in el) {
|
|
1873
|
+
return { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
|
|
1874
|
+
}
|
|
1875
|
+
return null;
|
|
1876
|
+
default:
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function computeBounds(elements, padding) {
|
|
1881
|
+
let minX = Infinity;
|
|
1882
|
+
let minY = Infinity;
|
|
1883
|
+
let maxX = -Infinity;
|
|
1884
|
+
let maxY = -Infinity;
|
|
1885
|
+
let found = false;
|
|
1886
|
+
for (const el of elements) {
|
|
1887
|
+
const rect = getElementRect(el);
|
|
1888
|
+
if (!rect) continue;
|
|
1889
|
+
found = true;
|
|
1890
|
+
minX = Math.min(minX, rect.x);
|
|
1891
|
+
minY = Math.min(minY, rect.y);
|
|
1892
|
+
maxX = Math.max(maxX, rect.x + rect.w);
|
|
1893
|
+
maxY = Math.max(maxY, rect.y + rect.h);
|
|
1894
|
+
}
|
|
1895
|
+
if (!found) return null;
|
|
1896
|
+
return {
|
|
1897
|
+
x: minX - padding,
|
|
1898
|
+
y: minY - padding,
|
|
1899
|
+
w: maxX - minX + padding * 2,
|
|
1900
|
+
h: maxY - minY + padding * 2
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
function renderNoteOnCanvas(ctx, note) {
|
|
1904
|
+
const { x, y } = note.position;
|
|
1905
|
+
const { w, h } = note.size;
|
|
1906
|
+
const r = 4;
|
|
1907
|
+
const pad = 8;
|
|
1908
|
+
ctx.save();
|
|
1909
|
+
ctx.fillStyle = note.backgroundColor;
|
|
1910
|
+
ctx.beginPath();
|
|
1911
|
+
ctx.moveTo(x + r, y);
|
|
1912
|
+
ctx.lineTo(x + w - r, y);
|
|
1913
|
+
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
1914
|
+
ctx.lineTo(x + w, y + h - r);
|
|
1915
|
+
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
1916
|
+
ctx.lineTo(x + r, y + h);
|
|
1917
|
+
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
1918
|
+
ctx.lineTo(x, y + r);
|
|
1919
|
+
ctx.arcTo(x, y, x + r, y, r);
|
|
1920
|
+
ctx.closePath();
|
|
1921
|
+
ctx.fill();
|
|
1922
|
+
if (note.text) {
|
|
1923
|
+
ctx.fillStyle = note.textColor;
|
|
1924
|
+
ctx.font = "14px system-ui, sans-serif";
|
|
1925
|
+
ctx.textBaseline = "top";
|
|
1926
|
+
wrapText(ctx, note.text, x + pad, y + pad, w - pad * 2, 18);
|
|
1927
|
+
}
|
|
1928
|
+
ctx.restore();
|
|
1929
|
+
}
|
|
1930
|
+
function renderTextOnCanvas(ctx, text) {
|
|
1931
|
+
if (!text.text) return;
|
|
1932
|
+
ctx.save();
|
|
1933
|
+
ctx.fillStyle = text.color;
|
|
1934
|
+
ctx.font = `${text.fontSize}px system-ui, sans-serif`;
|
|
1935
|
+
ctx.textBaseline = "top";
|
|
1936
|
+
ctx.textAlign = text.textAlign;
|
|
1937
|
+
const pad = 2;
|
|
1938
|
+
let textX = text.position.x + pad;
|
|
1939
|
+
if (text.textAlign === "center") {
|
|
1940
|
+
textX = text.position.x + text.size.w / 2;
|
|
1941
|
+
} else if (text.textAlign === "right") {
|
|
1942
|
+
textX = text.position.x + text.size.w - pad;
|
|
1943
|
+
}
|
|
1944
|
+
const lineHeight = text.fontSize * 1.4;
|
|
1945
|
+
const lines = text.text.split("\n");
|
|
1946
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1947
|
+
const line = lines[i];
|
|
1948
|
+
if (line !== void 0) {
|
|
1949
|
+
ctx.fillText(line, textX, text.position.y + pad + i * lineHeight);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
ctx.restore();
|
|
1953
|
+
}
|
|
1954
|
+
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
|
|
1955
|
+
const words = text.split(" ");
|
|
1956
|
+
let line = "";
|
|
1957
|
+
let offsetY = 0;
|
|
1958
|
+
for (const word of words) {
|
|
1959
|
+
const testLine = line ? `${line} ${word}` : word;
|
|
1960
|
+
const metrics = ctx.measureText(testLine);
|
|
1961
|
+
if (metrics.width > maxWidth && line) {
|
|
1962
|
+
ctx.fillText(line, x, y + offsetY);
|
|
1963
|
+
line = word;
|
|
1964
|
+
offsetY += lineHeight;
|
|
1965
|
+
} else {
|
|
1966
|
+
line = testLine;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
if (line) {
|
|
1970
|
+
ctx.fillText(line, x, y + offsetY);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
function renderGridForBounds(ctx, grid, bounds) {
|
|
1974
|
+
const visibleBounds = {
|
|
1975
|
+
minX: bounds.x,
|
|
1976
|
+
minY: bounds.y,
|
|
1977
|
+
maxX: bounds.x + bounds.w,
|
|
1978
|
+
maxY: bounds.y + bounds.h
|
|
1979
|
+
};
|
|
1980
|
+
if (grid.gridType === "hex") {
|
|
1981
|
+
renderHexGrid(
|
|
1982
|
+
ctx,
|
|
1983
|
+
visibleBounds,
|
|
1984
|
+
grid.cellSize,
|
|
1985
|
+
grid.hexOrientation,
|
|
1986
|
+
grid.strokeColor,
|
|
1987
|
+
grid.strokeWidth,
|
|
1988
|
+
grid.opacity
|
|
1989
|
+
);
|
|
1990
|
+
} else {
|
|
1991
|
+
renderSquareGrid(
|
|
1992
|
+
ctx,
|
|
1993
|
+
visibleBounds,
|
|
1994
|
+
grid.cellSize,
|
|
1995
|
+
grid.strokeColor,
|
|
1996
|
+
grid.strokeWidth,
|
|
1997
|
+
grid.opacity
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
function loadImages(elements) {
|
|
2002
|
+
const imageElements = elements.filter(
|
|
2003
|
+
(el) => el.type === "image" && "src" in el
|
|
2004
|
+
);
|
|
2005
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2006
|
+
if (imageElements.length === 0) return Promise.resolve(cache);
|
|
2007
|
+
return new Promise((resolve) => {
|
|
2008
|
+
let remaining = imageElements.length;
|
|
2009
|
+
const done = () => {
|
|
2010
|
+
remaining--;
|
|
2011
|
+
if (remaining <= 0) resolve(cache);
|
|
2012
|
+
};
|
|
2013
|
+
for (const el of imageElements) {
|
|
2014
|
+
const img = new Image();
|
|
2015
|
+
img.onload = () => {
|
|
2016
|
+
cache.set(el.id, img);
|
|
2017
|
+
done();
|
|
2018
|
+
};
|
|
2019
|
+
img.onerror = done;
|
|
2020
|
+
img.src = el.src;
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
async function exportImage(store, options = {}, layerManager) {
|
|
2025
|
+
const scale = options.scale ?? 2;
|
|
2026
|
+
const padding = options.padding ?? 0;
|
|
2027
|
+
const background = options.background ?? "#ffffff";
|
|
2028
|
+
const filter = options.filter;
|
|
2029
|
+
const allElements = store.getAll();
|
|
2030
|
+
let visibleElements = layerManager ? allElements.filter((el) => layerManager.isLayerVisible(el.layerId)) : allElements;
|
|
2031
|
+
if (filter) {
|
|
2032
|
+
visibleElements = visibleElements.filter(filter);
|
|
2033
|
+
}
|
|
2034
|
+
const bounds = computeBounds(visibleElements, padding);
|
|
2035
|
+
if (!bounds) return null;
|
|
2036
|
+
const imageCache = await loadImages(visibleElements);
|
|
2037
|
+
const canvas = document.createElement("canvas");
|
|
2038
|
+
canvas.width = Math.ceil(bounds.w * scale);
|
|
2039
|
+
canvas.height = Math.ceil(bounds.h * scale);
|
|
2040
|
+
const ctx = canvas.getContext("2d");
|
|
2041
|
+
if (!ctx) return null;
|
|
2042
|
+
ctx.scale(scale, scale);
|
|
2043
|
+
ctx.translate(-bounds.x, -bounds.y);
|
|
2044
|
+
ctx.fillStyle = background;
|
|
2045
|
+
ctx.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
|
|
2046
|
+
const renderer = new ElementRenderer();
|
|
2047
|
+
renderer.setStore(store);
|
|
2048
|
+
const grids = [];
|
|
2049
|
+
for (const el of visibleElements) {
|
|
2050
|
+
if (el.type === "grid") {
|
|
2051
|
+
grids.push(el);
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (el.type === "note") {
|
|
2055
|
+
renderNoteOnCanvas(ctx, el);
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
if (el.type === "text") {
|
|
2059
|
+
renderTextOnCanvas(ctx, el);
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
if (el.type === "html") {
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (el.type === "image") {
|
|
2066
|
+
const img = imageCache.get(el.id);
|
|
2067
|
+
if (img) {
|
|
2068
|
+
ctx.drawImage(img, el.position.x, el.position.y, el.size.w, el.size.h);
|
|
2069
|
+
}
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
renderer.renderCanvasElement(ctx, el);
|
|
2073
|
+
}
|
|
2074
|
+
for (const grid of grids) {
|
|
2075
|
+
renderGridForBounds(ctx, grid, bounds);
|
|
2076
|
+
}
|
|
2077
|
+
return new Promise((resolve) => {
|
|
2078
|
+
canvas.toBlob((blob) => resolve(blob), "image/png");
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1829
2082
|
// src/layers/layer-manager.ts
|
|
1830
2083
|
var LayerManager = class {
|
|
1831
2084
|
constructor(store) {
|
|
@@ -2087,6 +2340,9 @@ var Viewport = class {
|
|
|
2087
2340
|
exportJSON() {
|
|
2088
2341
|
return JSON.stringify(this.exportState());
|
|
2089
2342
|
}
|
|
2343
|
+
async exportImage(options) {
|
|
2344
|
+
return exportImage(this.store, options, this.layerManager);
|
|
2345
|
+
}
|
|
2090
2346
|
loadState(state) {
|
|
2091
2347
|
this.historyRecorder.pause();
|
|
2092
2348
|
this.noteEditor.destroy(this.store);
|
|
@@ -2095,6 +2351,7 @@ var Viewport = class {
|
|
|
2095
2351
|
if (state.layers && state.layers.length > 0) {
|
|
2096
2352
|
this.layerManager.loadSnapshot(state.layers);
|
|
2097
2353
|
}
|
|
2354
|
+
this.reattachHtmlContent();
|
|
2098
2355
|
this.history.clear();
|
|
2099
2356
|
this.historyRecorder.resume();
|
|
2100
2357
|
this.camera.moveTo(state.camera.position.x, state.camera.position.y);
|
|
@@ -2126,7 +2383,13 @@ var Viewport = class {
|
|
|
2126
2383
|
return image.id;
|
|
2127
2384
|
}
|
|
2128
2385
|
addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
|
|
2129
|
-
const
|
|
2386
|
+
const domId = dom.id || void 0;
|
|
2387
|
+
const el = createHtmlElement({
|
|
2388
|
+
position,
|
|
2389
|
+
size,
|
|
2390
|
+
domId,
|
|
2391
|
+
layerId: this.layerManager.activeLayerId
|
|
2392
|
+
});
|
|
2130
2393
|
this.htmlContent.set(el.id, dom);
|
|
2131
2394
|
this.historyRecorder.begin();
|
|
2132
2395
|
this.store.add(el);
|
|
@@ -2495,6 +2758,16 @@ var Viewport = class {
|
|
|
2495
2758
|
this.htmlContent.clear();
|
|
2496
2759
|
this.requestRender();
|
|
2497
2760
|
}
|
|
2761
|
+
reattachHtmlContent() {
|
|
2762
|
+
for (const el of this.store.getElementsByType("html")) {
|
|
2763
|
+
if (el.domId) {
|
|
2764
|
+
const dom = document.getElementById(el.domId);
|
|
2765
|
+
if (dom) {
|
|
2766
|
+
this.htmlContent.set(el.id, dom);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2498
2771
|
createWrapper() {
|
|
2499
2772
|
const el = document.createElement("div");
|
|
2500
2773
|
Object.assign(el.style, {
|
|
@@ -2753,10 +3026,11 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
2753
3026
|
const el = ctx.store.getById(elementId);
|
|
2754
3027
|
if (!el || el.type !== "arrow") return;
|
|
2755
3028
|
const threshold = BIND_THRESHOLD / ctx.camera.zoom;
|
|
3029
|
+
const layerFilter = (candidate) => candidate.layerId === el.layerId;
|
|
2756
3030
|
switch (handle) {
|
|
2757
3031
|
case "start": {
|
|
2758
3032
|
const excludeId = el.toBinding?.elementId;
|
|
2759
|
-
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
3033
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
2760
3034
|
if (target) {
|
|
2761
3035
|
const center = getElementCenter(target);
|
|
2762
3036
|
ctx.store.update(elementId, {
|
|
@@ -2775,7 +3049,7 @@ function applyArrowHandleDrag(handle, elementId, world, ctx) {
|
|
|
2775
3049
|
}
|
|
2776
3050
|
case "end": {
|
|
2777
3051
|
const excludeId = el.fromBinding?.elementId;
|
|
2778
|
-
const target = findBindTarget(world, ctx.store, threshold, excludeId);
|
|
3052
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
2779
3053
|
if (target) {
|
|
2780
3054
|
const center = getElementCenter(target);
|
|
2781
3055
|
ctx.store.update(elementId, {
|
|
@@ -2804,7 +3078,8 @@ function getArrowHandleDragTarget(handle, elementId, world, ctx) {
|
|
|
2804
3078
|
if (!el || el.type !== "arrow") return null;
|
|
2805
3079
|
const threshold = BIND_THRESHOLD / ctx.camera.zoom;
|
|
2806
3080
|
const excludeId = handle === "start" ? el.toBinding?.elementId : el.fromBinding?.elementId;
|
|
2807
|
-
const
|
|
3081
|
+
const layerFilter = (candidate) => candidate.layerId === el.layerId;
|
|
3082
|
+
const target = findBindTarget(world, ctx.store, threshold, excludeId, layerFilter);
|
|
2808
3083
|
if (!target) return null;
|
|
2809
3084
|
return getElementBounds(target);
|
|
2810
3085
|
}
|
|
@@ -3264,8 +3539,10 @@ var ArrowTool = class {
|
|
|
3264
3539
|
if (options.width !== void 0) this.width = options.width;
|
|
3265
3540
|
}
|
|
3266
3541
|
layerFilter(ctx) {
|
|
3267
|
-
|
|
3542
|
+
const activeLayerId = ctx.activeLayerId;
|
|
3543
|
+
if (!activeLayerId && !ctx.isLayerVisible && !ctx.isLayerLocked) return void 0;
|
|
3268
3544
|
return (el) => {
|
|
3545
|
+
if (activeLayerId && el.layerId !== activeLayerId) return false;
|
|
3269
3546
|
if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) return false;
|
|
3270
3547
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) return false;
|
|
3271
3548
|
return true;
|
|
@@ -3649,7 +3926,7 @@ var UpdateLayerCommand = class {
|
|
|
3649
3926
|
};
|
|
3650
3927
|
|
|
3651
3928
|
// src/index.ts
|
|
3652
|
-
var VERSION = "0.
|
|
3929
|
+
var VERSION = "0.8.0";
|
|
3653
3930
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3654
3931
|
0 && (module.exports = {
|
|
3655
3932
|
AddElementCommand,
|
|
@@ -3692,6 +3969,7 @@ var VERSION = "0.6.1";
|
|
|
3692
3969
|
createShape,
|
|
3693
3970
|
createStroke,
|
|
3694
3971
|
createText,
|
|
3972
|
+
exportImage,
|
|
3695
3973
|
exportState,
|
|
3696
3974
|
findBindTarget,
|
|
3697
3975
|
findBoundArrows,
|