added debugger
This commit is contained in:
parent
5227c0acea
commit
223c5f76e0
@ -35,7 +35,6 @@
|
|||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const data = e.target?.result;
|
const data = e.target?.result;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
console.log(file, data);
|
|
||||||
createFileViewState(file.name, data)
|
createFileViewState(file.name, data)
|
||||||
};
|
};
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
|
|||||||
@ -3,9 +3,8 @@
|
|||||||
import { UploadOutline } from 'flowbite-svelte-icons';
|
import { UploadOutline } from 'flowbite-svelte-icons';
|
||||||
import * as monaco from 'monaco-editor';
|
import * as monaco from 'monaco-editor';
|
||||||
import type ContentModel from '../models/ContentModel.svelte';
|
import type ContentModel from '../models/ContentModel.svelte';
|
||||||
import { getPDFOperator, opLookup } from '../models/OperatorList';
|
import { opLookup } from '../models/OperatorList';
|
||||||
import { OperatorList } from '../models/OperatorList.js';
|
import type FileViewState from '../models/FileViewState.svelte';
|
||||||
|
|
||||||
const opIdRegex = /opId-(\d+)/;
|
const opIdRegex = /opId-(\d+)/;
|
||||||
|
|
||||||
monaco.languages.register({ id: 'pdf-content-stream' });
|
monaco.languages.register({ id: 'pdf-content-stream' });
|
||||||
@ -63,20 +62,31 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
fState,
|
||||||
|
contentsUpdatedEvent,
|
||||||
height,
|
height,
|
||||||
save,
|
onBreakpointsChange = (breakpoints: Set<number>) => {},
|
||||||
contents
|
currentOgIdx = -1
|
||||||
}: {
|
}: {
|
||||||
|
fState: FileViewState;
|
||||||
|
contentsUpdatedEvent: () => void;
|
||||||
height: number;
|
height: number;
|
||||||
save: (updated_data: string) => void;
|
onBreakpointsChange?: (breakpoints: Set<number>) => void;
|
||||||
contents: ContentModel;
|
currentOgIdx?: number;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let editorContainer: HTMLElement;
|
let editorContainer: HTMLElement;
|
||||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||||
|
|
||||||
let isEdited = $state(false);
|
let contents: ContentModel | undefined;
|
||||||
let lastContents: ContentModel | undefined = $state(undefined);
|
let isEdited = false;
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
let breakpoints: Set<number> = $state(new Set());
|
||||||
|
let breakpointDecorations: monaco.editor.IEditorDecorationsCollection | null = null;
|
||||||
|
let currentOperationDecoration: monaco.editor.IEditorDecorationsCollection | null = null;
|
||||||
|
let lineToOperations: Map<number, number[]> = new Map();
|
||||||
|
let ogIdxToEditorIdx: Map<number, number> = new Map();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = monaco.editor.create(editorContainer, {
|
editor = monaco.editor.create(editorContainer, {
|
||||||
@ -87,18 +97,71 @@
|
|||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
lineDecorationsWidth: 8, // Further reducing margin after line numbers
|
||||||
|
lineNumbersMinChars: 3, // Controls minimum width of line number area
|
||||||
|
glyphMargin: true, // Enable the gutter for breakpoints
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add change listener to detect edits
|
|
||||||
editor.onDidChangeModelContent(() => {
|
editor.onDidChangeModelContent(() => {
|
||||||
isEdited = true;
|
isEdited = loaded;
|
||||||
|
updateContents();
|
||||||
});
|
});
|
||||||
|
|
||||||
loadContents(contents);
|
const gutterElement = editorContainer.querySelector('.margin');
|
||||||
|
if (gutterElement) {
|
||||||
|
gutterElement.classList.add('breakpoint-hover-area');
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastHoveredLine: number | null = null;
|
||||||
|
let hoverDecorationIds: string[] = [];
|
||||||
|
|
||||||
|
function clearHoverIndicators() {
|
||||||
|
if (hoverDecorationIds.length > 0) {
|
||||||
|
editor.removeDecorations(hoverDecorationIds);
|
||||||
|
hoverDecorationIds = [];
|
||||||
|
lastHoveredLine = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editorContainer.addEventListener('mouseleave', clearHoverIndicators);
|
||||||
|
|
||||||
|
editor.onMouseMove((e) => {
|
||||||
|
const currentLineNumber = e.target.position?.lineNumber || null;
|
||||||
|
|
||||||
|
if ((e.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS ||
|
||||||
|
e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN) &&
|
||||||
|
currentLineNumber) {
|
||||||
|
|
||||||
|
if (currentLineNumber !== lastHoveredLine || hoverDecorationIds.length === 0) {
|
||||||
|
clearHoverIndicators();
|
||||||
|
|
||||||
|
if (!breakpoints.has(currentLineNumber)) {
|
||||||
|
hoverDecorationIds = editor.deltaDecorations([], [{
|
||||||
|
range: new monaco.Range(currentLineNumber, 1, currentLineNumber, 1),
|
||||||
|
options: {
|
||||||
|
isWholeLine: false,
|
||||||
|
glyphMarginClassName: 'breakpoint-glyph-hover'
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastHoveredLine = currentLineNumber;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearHoverIndicators();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
editor.onMouseDown((e) => {
|
editor.onMouseDown((e) => {
|
||||||
if (!e.target.position) return;
|
if (!e.target.position) return;
|
||||||
|
|
||||||
|
if (e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN ||
|
||||||
|
e.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS) {
|
||||||
|
toggleBreakpointAtLine(e.target.position.lineNumber);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const decorations = editor.getModel()?.getDecorationsInRange({
|
const decorations = editor.getModel()?.getDecorationsInRange({
|
||||||
startLineNumber: e.target.position.lineNumber,
|
startLineNumber: e.target.position.lineNumber,
|
||||||
startColumn: e.target.position.column,
|
startColumn: e.target.position.column,
|
||||||
@ -110,34 +173,111 @@
|
|||||||
if (!match) {
|
if (!match) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(contents.opList.fnArray[+match[1]]);
|
|
||||||
console.log(contents.opList.argsArray[+match[1]]);
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
editorContainer.removeEventListener('mouseleave', clearHoverIndicators);
|
||||||
editor.dispose();
|
editor.dispose();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
loadContents(contents);
|
loadContents();
|
||||||
});
|
});
|
||||||
|
|
||||||
function addDecorations(contents: ContentModel) {
|
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (currentOgIdx >= 0) {
|
||||||
|
highlightCurrentOperation(currentOgIdx);
|
||||||
|
} else {
|
||||||
|
clearCurrentOperationHighlight();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function toggleBreakpointAtLine(lineNumber: number) {
|
||||||
|
if (!contents?.opList) return;
|
||||||
|
|
||||||
|
|
||||||
|
const operations = lineToOperations.get(lineNumber) || [];
|
||||||
|
|
||||||
|
if (operations.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasBreakpoint = operations.some(ogIdx => breakpoints.has(ogIdx));
|
||||||
|
|
||||||
|
if (hasBreakpoint) {
|
||||||
|
operations.forEach(ogIdx => {
|
||||||
|
breakpoints.delete(ogIdx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
operations.forEach(ogIdx => {
|
||||||
|
breakpoints.add(ogIdx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBreakpointDecorations();
|
||||||
|
|
||||||
|
breakpoints = new Set([...breakpoints]);
|
||||||
|
onBreakpointsChange(breakpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBreakpointDecorations() {
|
||||||
|
const model = editor.getModel();
|
||||||
|
if (!model || !contents?.opList) return;
|
||||||
|
|
||||||
|
const decorationsArray = [];
|
||||||
|
|
||||||
|
const linesWithBreakpoints = new Set<number>();
|
||||||
|
|
||||||
|
for (let i = 0; i < contents.opList.fnArray.length; i++) {
|
||||||
|
const operator = contents.opList.getOperatorAt(i);
|
||||||
|
|
||||||
|
if (breakpoints.has(operator.ogIdx) && operator.range) {
|
||||||
|
const startPos = model.getPositionAt(operator.range.start);
|
||||||
|
linesWithBreakpoints.add(startPos.lineNumber);
|
||||||
|
|
||||||
|
decorationsArray.push({
|
||||||
|
range: new monaco.Range(startPos.lineNumber, 1, startPos.lineNumber, 1),
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
className: 'breakpoint-line',
|
||||||
|
glyphMarginClassName: 'breakpoint-glyph',
|
||||||
|
glyphMarginHoverMessage: { value: `Breakpoint on operation: ${operator.getPDFOperator?.().name || 'Unknown'}` }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakpointDecorations) {
|
||||||
|
breakpointDecorations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
breakpointDecorations = editor.createDecorationsCollection(decorationsArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAllDecorations() {
|
||||||
|
const ids = editor.getModel()?.getAllDecorations().map(d => d.id) ?? [];
|
||||||
|
editor.removeDecorations(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDecorations(contents: ContentModel) {
|
||||||
|
removeAllDecorations();
|
||||||
if (contents.opList && contents.opList.fnArray.length > 0) {
|
if (contents.opList && contents.opList.fnArray.length > 0) {
|
||||||
const decorations = [];
|
const decorations = [];
|
||||||
|
|
||||||
|
lineToOperations.clear();
|
||||||
|
ogIdxToEditorIdx.clear();
|
||||||
|
|
||||||
for (let i = 0; i < contents.opList.fnArray.length; i++) {
|
for (let i = 0; i < contents.opList.fnArray.length; i++) {
|
||||||
const fnId = contents.opList.fnArray[i];
|
const operator = contents.opList.getOperatorAt(i);
|
||||||
const args = contents.opList.argsArray[i];
|
|
||||||
const range = contents.opList.rangeArray[i];
|
|
||||||
const operator = getPDFOperator(fnId);
|
|
||||||
const model = editor.getModel();
|
const model = editor.getModel();
|
||||||
if (model && operator && range) {
|
if (model && operator && operator.range) {
|
||||||
const startPos = model.getPositionAt(range[0]);
|
const startPos = model.getPositionAt(operator.range.start);
|
||||||
const endPos = model.getPositionAt(range[1]);
|
const endPos = model.getPositionAt(operator.range.end);
|
||||||
const monacoRange = new monaco.Range(
|
const monacoRange = new monaco.Range(
|
||||||
startPos.lineNumber,
|
startPos.lineNumber,
|
||||||
startPos.column,
|
startPos.column,
|
||||||
@ -145,45 +285,132 @@
|
|||||||
endPos.column
|
endPos.column
|
||||||
);
|
);
|
||||||
|
|
||||||
let className = `${operator.class} highlightOp opId-${i}`;
|
ogIdxToEditorIdx.set(operator.ogIdx, i);
|
||||||
// Add class-based decoration for operator highlighting
|
|
||||||
|
let className = `${operator.getPDFOperator?.().class || 'operator'} highlightOp opId-${i}`;
|
||||||
|
|
||||||
decorations.push({
|
decorations.push({
|
||||||
range: monacoRange,
|
range: monacoRange,
|
||||||
options: {
|
options: {
|
||||||
className: className,
|
className: className,
|
||||||
hoverMessage: { value: OperatorList.formatOperatorToMarkdown(fnId, args) }
|
hoverMessage: { value: operator.toMarkdown?.() || `Operator Index: ${i}, Original Index: ${operator.ogIdx}` }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lineNumber = startPos.lineNumber;
|
||||||
|
if (!lineToOperations.has(lineNumber)) {
|
||||||
|
lineToOperations.set(lineNumber, []);
|
||||||
|
}
|
||||||
|
lineToOperations.get(lineNumber)?.push(operator.ogIdx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply all decorations at once
|
|
||||||
editor.createDecorationsCollection(decorations);
|
editor.createDecorationsCollection(decorations);
|
||||||
|
|
||||||
|
updateBreakpointDecorations();
|
||||||
|
|
||||||
|
if (currentOgIdx >= 0) {
|
||||||
|
highlightCurrentOperation(currentOgIdx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadContents(contents: ContentModel | undefined) {
|
async function loadContents() {
|
||||||
if (!contents || !editor) return;
|
if (!fState.currentPageNumber || !editor || fState.currentPageNumber === contents?.pageNum) return;
|
||||||
|
loaded = false;
|
||||||
|
contents = await fState.document.getContents(fState.currentPageNumber);
|
||||||
|
|
||||||
if (lastContents === contents) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastContents = contents;
|
|
||||||
editor.setValue(contents.toDisplay());
|
editor.setValue(contents.toDisplay());
|
||||||
|
|
||||||
|
lineToOperations.clear();
|
||||||
|
ogIdxToEditorIdx.clear();
|
||||||
|
|
||||||
|
clearCurrentOperationHighlight();
|
||||||
|
breakpointDecorations?.clear();
|
||||||
|
|
||||||
|
updateDecorations(contents);
|
||||||
isEdited = false;
|
isEdited = false;
|
||||||
|
loaded = true;
|
||||||
addDecorations(contents);
|
breakpoints = new Set();
|
||||||
|
onBreakpointsChange(breakpoints);
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
|
async function updateContents() {
|
||||||
|
if (!fState.currentPageNumber || !loaded || !isEdited) return;
|
||||||
|
await fState.document.updateContents(fState.currentPageNumber, editor.getValue())
|
||||||
|
const contents = await fState.document.getContents(fState.currentPageNumber);
|
||||||
|
updateDecorations(contents);
|
||||||
|
isEdited = false;
|
||||||
|
contentsUpdatedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightCurrentOperation(ogIdx: number) {
|
||||||
|
if (!contents?.opList || !editor.getModel()) return;
|
||||||
|
|
||||||
|
clearCurrentOperationHighlight();
|
||||||
|
|
||||||
|
const editorIdx = findEditorIdxForOgIdx(ogIdx);
|
||||||
|
if (editorIdx === undefined) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const operator = contents.opList.getOperatorAt(editorIdx);
|
||||||
|
if (!operator || !operator.range) return;
|
||||||
|
|
||||||
|
const model = editor.getModel();
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
const startPos = model.getPositionAt(operator.range.start);
|
||||||
|
const endPos = model.getPositionAt(operator.range.end);
|
||||||
|
|
||||||
|
currentOperationDecoration = editor.createDecorationsCollection([{
|
||||||
|
range: new monaco.Range(
|
||||||
|
startPos.lineNumber,
|
||||||
|
startPos.column,
|
||||||
|
endPos.lineNumber,
|
||||||
|
endPos.column
|
||||||
|
),
|
||||||
|
options: {
|
||||||
|
className: 'current-operation-highlight',
|
||||||
|
isWholeLine: false,
|
||||||
|
inlineClassName: 'current-operation-inline',
|
||||||
|
hoverMessage: { value: 'Current operation (execution paused)' }
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
editor.revealPositionInCenter(startPos);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error highlighting current operation:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEditorIdxForOgIdx(ogIdx: number): number | undefined {
|
||||||
|
return ogIdxToEditorIdx.get(ogIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCurrentOperationHighlight() {
|
||||||
|
if (currentOperationDecoration) {
|
||||||
|
currentOperationDecoration.clear();
|
||||||
|
currentOperationDecoration = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
<div class="breakpoints-list">
|
||||||
|
<h3>Breakpoints</h3>
|
||||||
|
<ul>
|
||||||
|
{#each Array.from(breakpoints).sort((a, b) => a - b) as opIdx}
|
||||||
|
<li>Operation {opIdx}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<div bind:this={editorContainer} style:height={height + "px"}></div>
|
<div bind:this={editorContainer} style:height={height + "px"}></div>
|
||||||
{#if isEdited}
|
{#if isEdited}
|
||||||
<button onclick={() => save(editor.getValue())} class="save-button">
|
<button onclick={() => updateContents()} class="save-button">
|
||||||
<UploadOutline size="xl" />
|
<UploadOutline size="xl" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
@ -191,7 +418,7 @@
|
|||||||
@apply p-2 bg-forge-sec text-forge-text border-t border-forge-bound cursor-pointer;
|
@apply p-2 bg-forge-sec text-forge-text border-t border-forge-bound cursor-pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 2em;
|
right: 2em;
|
||||||
bottom: 0px;
|
bottom: 0;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
@ -204,4 +431,70 @@
|
|||||||
@apply border border-forge-sec rounded;
|
@apply border border-forge-sec rounded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.current-operation-highlight) {
|
||||||
|
@apply border-2 border-orange-400 rounded bg-opacity-20 bg-orange-400;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.current-operation-inline) {
|
||||||
|
@apply font-bold text-orange-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(251, 146, 60, 0.1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: rgba(251, 146, 60, 0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgba(251, 146, 60, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.breakpoint-glyph) {
|
||||||
|
margin-left: 3px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 4px solid transparent;
|
||||||
|
border-bottom: 4px solid transparent;
|
||||||
|
border-left: 6px solid #c65252;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.breakpoint-glyph-hover) {
|
||||||
|
margin-left: 3px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 4px solid transparent;
|
||||||
|
border-bottom: 4px solid transparent;
|
||||||
|
border-left: 6px solid rgba(198, 82, 82, 0.4);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.breakpoint-line) {
|
||||||
|
background-color: rgba(198, 82, 82, 0.2); /* Light red background */
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.breakpoint-hover-area) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breakpoints-list {
|
||||||
|
@apply p-3 bg-forge-prim text-forge-text border border-forge-bound rounded;
|
||||||
|
position: absolute;
|
||||||
|
right: 2em;
|
||||||
|
top: 2em;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breakpoints-list h3 {
|
||||||
|
@apply font-bold mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breakpoints-list ul {
|
||||||
|
@apply list-disc pl-4;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,27 +1,169 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { RefreshOutline } from "flowbite-svelte-icons";
|
import ZoomControls from '../components/ZoomControls.svelte';
|
||||||
import ZoomableContainer from './ZoomableContainer.svelte';
|
import type { ViewState } from '../models/ViewState';
|
||||||
|
import {
|
||||||
|
RefreshOutline
|
||||||
|
} from 'flowbite-svelte-icons';
|
||||||
|
type ZoomableProps = {
|
||||||
|
minZoom?: number;
|
||||||
|
maxZoom?: number;
|
||||||
|
zoomStep?: number;
|
||||||
|
imgUrl?: string;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
minZoom = 0.1,
|
||||||
|
maxZoom = 30,
|
||||||
|
zoomStep = 0.1,
|
||||||
|
imgUrl,
|
||||||
|
height
|
||||||
|
}: ZoomableProps = $props();
|
||||||
|
|
||||||
|
let container = $state<HTMLDivElement>();
|
||||||
|
|
||||||
|
let viewState: ViewState = $state({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
scale: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
type DragState = {
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let dragState: DragState | undefined = $state(undefined);
|
||||||
|
|
||||||
|
function handleZoom(event: WheelEvent) {
|
||||||
|
if (!container || !event.ctrlKey) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const delta = -Math.sign(event.deltaY);
|
||||||
|
const newScale = Math.min(
|
||||||
|
Math.max(viewState.scale + delta * zoomStep * viewState.scale, minZoom),
|
||||||
|
maxZoom
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newScale !== viewState.scale) {
|
||||||
|
const x = event.clientX;
|
||||||
|
const y = event.clientY;
|
||||||
|
|
||||||
|
const scaleChange = newScale / viewState.scale;
|
||||||
|
container.scrollLeft = x * scaleChange - x + container.scrollLeft;
|
||||||
|
container.scrollTop = y * scaleChange - y + container.scrollTop;
|
||||||
|
viewState.scale = newScale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag handlers
|
||||||
|
function handleDragStart(event: MouseEvent) {
|
||||||
|
if (!container || event.button) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
dragState = {
|
||||||
|
startX: event.pageX - viewState.x,
|
||||||
|
startY: event.pageY - viewState.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragMove(event: MouseEvent) {
|
||||||
|
if (!container || !dragState) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const x = event.pageX - container.offsetLeft;
|
||||||
|
const y = event.pageY - container.offsetTop;
|
||||||
|
|
||||||
|
viewState.x = (x - dragState.startX);
|
||||||
|
viewState.y = (y - dragState.startY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
dragState = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function zoomIn() {
|
||||||
|
const newScale = Math.min(viewState.scale + (zoomStep * viewState.scale), maxZoom);
|
||||||
|
if (newScale !== viewState.scale) {
|
||||||
|
viewState.scale = newScale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomOut() {
|
||||||
|
const newScale = Math.max(viewState.scale - (zoomStep * viewState.scale), minZoom);
|
||||||
|
if (newScale !== viewState.scale) {
|
||||||
|
viewState.scale = newScale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetZoom() {
|
||||||
|
viewState.scale = 1;
|
||||||
|
if (container) {
|
||||||
|
viewState.x = 0;
|
||||||
|
viewState.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listener management
|
||||||
|
$effect(() => {
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener('wheel', handleZoom, { passive: false });
|
||||||
|
return () => container?.removeEventListener('wheel', handleZoom);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let { img, height }: { img: string | undefined; height: number } =
|
|
||||||
$props();
|
|
||||||
</script>
|
</script>
|
||||||
|
<div class="relative w-full h-full">
|
||||||
<div class="page-container" style:height={height + "px"}>
|
<ZoomControls
|
||||||
{#if !img}
|
scale={viewState.scale}
|
||||||
<div class="loading-container">
|
onZoomIn={() => zoomIn()}
|
||||||
<RefreshOutline class="animate-spin" size="xl" />
|
onZoomOut={() => zoomOut()}
|
||||||
</div>
|
{resetZoom}
|
||||||
|
showFit={false}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="container"
|
||||||
|
bind:this={container}
|
||||||
|
onmousedown={handleDragStart}
|
||||||
|
onmousemove={handleDragMove}
|
||||||
|
onmouseup={handleDragEnd}
|
||||||
|
onmouseleave={handleDragEnd}
|
||||||
|
role="presentation"
|
||||||
|
style:height={height + "px"}
|
||||||
|
style:max-height={height + "px"}
|
||||||
|
>
|
||||||
|
{#if !imgUrl}
|
||||||
|
<RefreshOutline class="animate-spin" size="xl"></RefreshOutline>
|
||||||
{:else}
|
{:else}
|
||||||
<ZoomableContainer imgUrl={img} {height}>
|
<img
|
||||||
</ZoomableContainer>
|
class="zoomimage"
|
||||||
|
alt="xobject"
|
||||||
|
src={imgUrl}
|
||||||
|
style:transform="translate({viewState.x}px, {viewState.y}px) scale({viewState.scale})"
|
||||||
|
style:max-height={height + "px"}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.page-container {
|
.container {
|
||||||
@apply w-full bg-forge-dark relative m-auto;
|
@apply w-full h-full bg-forge-dark;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: grab;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.loading-container {
|
|
||||||
@apply flex items-center justify-center h-full;
|
.container:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.zoomimage {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,43 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ZoomControls from '../components/ZoomControls.svelte';
|
import ZoomControls from '../components/ZoomControls.svelte';
|
||||||
|
import type FileViewState from '../models/FileViewState.svelte';
|
||||||
|
import { PageRenderState } from '../models/PageRenderState.svelte';
|
||||||
|
|
||||||
type ZoomableProps = {
|
type ZoomableProps = {
|
||||||
minZoom?: number;
|
minZoom?: number;
|
||||||
maxZoom?: number;
|
maxZoom?: number;
|
||||||
zoomStep?: number;
|
zoomStep?: number;
|
||||||
imgUrl?: string;
|
imgUrl?: string;
|
||||||
canvas?: HTMLCanvasElement;
|
fState: FileViewState;
|
||||||
height?: string | number;
|
height?: number;
|
||||||
|
render: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
minZoom = 0.1,
|
minZoom = 0.1,
|
||||||
maxZoom = 10,
|
maxZoom = 20,
|
||||||
zoomStep = 0.1,
|
zoomStep = 0.1,
|
||||||
imgUrl,
|
fState,
|
||||||
canvas = $bindable(),
|
height,
|
||||||
height
|
render = $bindable()
|
||||||
}: ZoomableProps = $props();
|
}: ZoomableProps = $props();
|
||||||
|
|
||||||
let currentImgUrl = $state(imgUrl);
|
|
||||||
let image: HTMLImageElement | undefined = $state(undefined);
|
let image: HTMLImageElement | undefined = $state(undefined);
|
||||||
let container = $state<HTMLDivElement>();
|
let container = $state<HTMLDivElement>();
|
||||||
|
let canvas = $state<HTMLCanvasElement>();
|
||||||
|
|
||||||
type ViewState = {
|
let renderState: PageRenderState | undefined = $derived(canvas && fState ? new PageRenderState(fState, canvas) : undefined)
|
||||||
x: number;
|
|
||||||
y: number;
|
let scale = $derived(renderState ? renderState.scale : 1);
|
||||||
scale: number;
|
|
||||||
|
$effect(() => {
|
||||||
|
if (renderState) {
|
||||||
|
render = () => {renderState.renderFrame();};
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let viewState: ViewState = $state({
|
$effect(() => {
|
||||||
x: 0,
|
if (renderState && fState.currentPageNumber) {
|
||||||
y: 0,
|
renderState.updatePage(fState.currentPageNumber).then(() => fitCanvasToPage())
|
||||||
scale: 1
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// DOM refs
|
let viewState = $state({
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
});
|
||||||
|
|
||||||
// Drag state
|
|
||||||
type DragState = {
|
type DragState = {
|
||||||
startX: number;
|
startX: number;
|
||||||
startY: number;
|
startY: number;
|
||||||
@ -45,25 +53,24 @@
|
|||||||
|
|
||||||
let dragState: DragState | undefined = $state(undefined);
|
let dragState: DragState | undefined = $state(undefined);
|
||||||
|
|
||||||
// Zoom handlers
|
|
||||||
function handleZoom(event: WheelEvent) {
|
function handleZoom(event: WheelEvent) {
|
||||||
if (!container || !event.ctrlKey) return;
|
if (!container || !event.ctrlKey) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const delta = -Math.sign(event.deltaY);
|
const delta = -Math.sign(event.deltaY);
|
||||||
const newScale = Math.min(
|
const newScale = Math.min(
|
||||||
Math.max(viewState.scale + delta * zoomStep * viewState.scale, minZoom),
|
Math.max(scale + delta * zoomStep * scale, minZoom),
|
||||||
maxZoom
|
maxZoom
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newScale !== viewState.scale) {
|
if (newScale !== scale) {
|
||||||
const x = event.clientX;
|
const x = event.clientX;
|
||||||
const y = event.clientY;
|
const y = event.clientY;
|
||||||
|
|
||||||
const scaleChange = newScale / viewState.scale;
|
const scaleChange = newScale / scale;
|
||||||
container.scrollLeft = x * scaleChange - x + container.scrollLeft;
|
container.scrollLeft = x * scaleChange - x + container.scrollLeft;
|
||||||
container.scrollTop = y * scaleChange - y + container.scrollTop;
|
container.scrollTop = y * scaleChange - y + container.scrollTop;
|
||||||
viewState.scale = newScale;
|
renderState?.updateScale(newScale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,25 +102,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fitCanvasToPage() {
|
function fitCanvasToPage() {
|
||||||
if (!canvas || !container) return;
|
if (!renderState || !container) return;
|
||||||
const w = canvas.width;
|
const w = renderState.width ?? 682;
|
||||||
const h = canvas.height;
|
const h = renderState.height ?? 798;
|
||||||
const targetWidth = container.offsetWidth;
|
const targetWidth = container.offsetWidth;
|
||||||
const targetHeight = container.offsetHeight;
|
const targetHeight = container.offsetHeight;
|
||||||
|
|
||||||
const wScale = targetWidth / w;
|
const wScale = targetWidth / w;
|
||||||
const hScale = targetHeight / h;
|
const hScale = targetHeight / h;
|
||||||
viewState.scale = Math.min(maxZoom, Math.max(minZoom, Math.min(wScale, hScale)));
|
const newScale = Math.min(maxZoom, Math.max(minZoom, Math.min(wScale, hScale)));
|
||||||
viewState.x = 0;
|
viewState.x = 0;
|
||||||
viewState.y = 0;
|
viewState.y = 0;
|
||||||
|
renderState?.updateScale(newScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomIn() {
|
||||||
|
const newScale = Math.min(scale + (zoomStep * scale), maxZoom);
|
||||||
|
if (newScale !== scale) {
|
||||||
|
renderState?.updateScale(newScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomOut() {
|
||||||
|
const newScale = Math.max(scale - (zoomStep * scale), minZoom);
|
||||||
|
if (newScale !== scale) {
|
||||||
|
renderState?.updateScale(newScale);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetZoom() {
|
function resetZoom() {
|
||||||
viewState.scale = 1;
|
|
||||||
if (container) {
|
if (container) {
|
||||||
viewState.x = 0;
|
viewState.x = 0;
|
||||||
viewState.y = 0;
|
viewState.y = 0;
|
||||||
}
|
}
|
||||||
|
renderState?.updateScale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event listener management
|
// Event listener management
|
||||||
@ -124,20 +146,12 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (canvas) {
|
|
||||||
currentImgUrl = canvas.toDataURL();
|
|
||||||
} else if (imgUrl) {
|
|
||||||
currentImgUrl = imgUrl;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<div class="relative w-full h-full">
|
<div class="relative w-full h-full">
|
||||||
<ZoomControls
|
<ZoomControls
|
||||||
scale={viewState.scale}
|
scale={scale}
|
||||||
onZoomIn={() => (viewState.scale = Math.min(viewState.scale + (zoomStep * viewState.scale), maxZoom))}
|
onZoomIn={() => zoomIn()}
|
||||||
onZoomOut={() => (viewState.scale = Math.max(viewState.scale - (zoomStep * viewState.scale), minZoom))}
|
onZoomOut={() => zoomOut()}
|
||||||
{resetZoom}
|
{resetZoom}
|
||||||
fit={fitCanvasToPage}
|
fit={fitCanvasToPage}
|
||||||
showFit={!image}
|
showFit={!image}
|
||||||
@ -153,23 +167,11 @@
|
|||||||
style:height={height + "px"}
|
style:height={height + "px"}
|
||||||
style:max-height={height + "px"}
|
style:max-height={height + "px"}
|
||||||
>
|
>
|
||||||
{#if imgUrl}
|
|
||||||
<img
|
|
||||||
class="zoomimage"
|
|
||||||
bind:this={image}
|
|
||||||
alt="rendered-page"
|
|
||||||
src={currentImgUrl}
|
|
||||||
style:transform="translate({viewState.x}px, {viewState.y}px) scale({viewState.scale})"
|
|
||||||
style:max-height={height + "px"}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<canvas
|
<canvas
|
||||||
bind:this={canvas}
|
bind:this={canvas}
|
||||||
class="zoomimage"
|
style:transform="translate({viewState.x}px, {viewState.y}px)"
|
||||||
style:transform="translate({viewState.x}px, {viewState.y}px) scale({viewState.scale})"
|
|
||||||
>
|
>
|
||||||
</canvas>
|
</canvas>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -187,8 +189,4 @@
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoomimage {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@ -1,66 +1,177 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||||
|
import { BugOutline } from "flowbite-svelte-icons";
|
||||||
import type FileViewState from '../models/FileViewState.svelte';
|
import type FileViewState from '../models/FileViewState.svelte';
|
||||||
import ContentModel from '../models/ContentModel.svelte';
|
|
||||||
import type DocumentWorker from '../models/Document.svelte';
|
|
||||||
import ContentEditor from './ContentEditor.svelte';
|
import ContentEditor from './ContentEditor.svelte';
|
||||||
import ZoomableContainer from './ZoomableContainer.svelte';
|
import PageRenderView from './PageRenderView.svelte';
|
||||||
|
import { DebugStepper } from '../models/Stepper.svelte.js';
|
||||||
let display: HTMLCanvasElement;
|
|
||||||
|
|
||||||
let { fState, height }: { fState: FileViewState, height: number } = $props();
|
let { fState, height }: { fState: FileViewState, height: number } = $props();
|
||||||
let renderer: DocumentWorker = $derived(fState.document);
|
|
||||||
let pageNum = $derived(fState.currentPageNumber);
|
let pageNum = $derived(fState.currentPageNumber);
|
||||||
let contents: ContentModel | undefined = $state(undefined);
|
let render: () => void = $state(() => {});
|
||||||
|
let stepper: DebugStepper | undefined = $state();
|
||||||
|
let breakpoints: Set<number> = $state(new Set<number>());
|
||||||
|
|
||||||
$effect(() => {
|
function contentsUpdatedEvent() {
|
||||||
refresh();
|
render();
|
||||||
});
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
if (pageNum) {
|
|
||||||
renderer.getContents(pageNum).then(parts => {
|
|
||||||
contents = parts;
|
|
||||||
renderer.renderPage(pageNum, display);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} else {
|
function startDebug() {
|
||||||
contents = undefined;
|
if (!pageNum) return;
|
||||||
|
stepper = new DebugStepper(pageNum, breakpoints);
|
||||||
|
fState.document.stepperManager.register(stepper);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBreakpointsChange(newBreakpoints: Set<number>) {
|
||||||
|
breakpoints = newBreakpoints;
|
||||||
|
stepper?.updateBreakPoints(breakpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
function continueExecution() {
|
||||||
|
if (stepper) {
|
||||||
|
stepper.continue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update(newData: string) {
|
function stepExecution() {
|
||||||
if (pageNum) {
|
if (stepper) {
|
||||||
await fState.document.updateContents(pageNum, newData);
|
stepper.step();
|
||||||
refresh();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopDebugging() {
|
||||||
|
if (stepper) {
|
||||||
|
fState.document.stepperManager.unregister(stepper);
|
||||||
|
stepper = undefined;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{#if pageNum}
|
{#if pageNum}
|
||||||
<Splitpanes theme="forge-movable">
|
<Splitpanes theme="forge-movable">
|
||||||
<Pane minSize={1}>
|
<Pane minSize={1}>
|
||||||
<div class="overflow-hidden">
|
<div class="controls">
|
||||||
{#if contents}
|
<h1 class="text-forge-text font-semibold">Contents</h1>
|
||||||
<ContentEditor save={(newData) => update(newData)} contents={contents} height={height - 1}></ContentEditor>
|
<button class="debugButton m-1" on:click={startDebug}>
|
||||||
|
<BugOutline size="md"></BugOutline>
|
||||||
|
</button>
|
||||||
|
{#if breakpoints.size > 0}
|
||||||
|
<span class="breakpoint-count">{breakpoints.size}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden relative h-full">
|
||||||
|
<ContentEditor
|
||||||
|
{fState}
|
||||||
|
height={stepper ? (height - 101) : (height - 1)}
|
||||||
|
{contentsUpdatedEvent}
|
||||||
|
onBreakpointsChange={handleBreakpointsChange}
|
||||||
|
currentOgIdx={stepper?.currentIdx}
|
||||||
|
></ContentEditor>
|
||||||
|
{#if stepper}
|
||||||
|
<div class="debug-terminal">
|
||||||
|
<div class="debug-terminal-header">
|
||||||
|
<h2 class="text-forge-text font-semibold">Debug Terminal</h2>
|
||||||
|
<div class="debug-info">
|
||||||
|
{#if stepper.onBreak}
|
||||||
|
<span class="break-indicator">Paused at operation {stepper.currentIdx}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="running-indicator">Running...</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="debug-controls">
|
||||||
|
<button class="debug-button continue" on:click={continueExecution}>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
<button class="debug-button step" on:click={stepExecution}>
|
||||||
|
Step
|
||||||
|
</button>
|
||||||
|
<button class="debug-button stop" on:click={stopDebugging}>
|
||||||
|
Stop Debugging
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
<Pane minSize={1}>
|
<Pane minSize={1}>
|
||||||
<ZoomableContainer bind:canvas={display} {height}></ZoomableContainer>
|
<PageRenderView bind:render {fState} {height}></PageRenderView>
|
||||||
</Pane>
|
</Pane>
|
||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
{:else }
|
{:else }
|
||||||
<h1>Please select a Page!</h1>
|
<h1>Please select a Page!</h1>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.scroller {
|
|
||||||
@apply w-full h-full bg-forge-dark;
|
.controls {
|
||||||
overflow: auto;
|
@apply bg-forge-dark border-forge-prim border-b flex flex-row;
|
||||||
display: flex;
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
}
|
}
|
||||||
.container {
|
.debugButton {
|
||||||
display: contents;
|
@apply bg-forge-prim rounded-sm hover:bg-forge-acc text-green-500 p-1;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breakpoint-count {
|
||||||
|
@apply bg-red-600 text-white text-xs font-bold px-1.5 py-0.5 rounded-full;
|
||||||
|
position: absolute;
|
||||||
|
right: 2.5rem;
|
||||||
|
top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-terminal {
|
||||||
|
@apply bg-forge-dark border-forge-prim border-t;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-terminal-header {
|
||||||
|
@apply bg-forge-prim flex flex-row justify-between items-center px-3 py-1;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-info {
|
||||||
|
@apply flex flex-row items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-indicator {
|
||||||
|
@apply text-yellow-300 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.running-indicator {
|
||||||
|
@apply text-green-400 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-controls {
|
||||||
|
@apply flex flex-row p-2 space-x-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-button {
|
||||||
|
@apply px-4 py-2 rounded-md font-medium text-white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-button.continue {
|
||||||
|
@apply bg-green-600 hover:bg-green-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-button.step {
|
||||||
|
@apply bg-blue-600 hover:bg-blue-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-button.stop {
|
||||||
|
@apply bg-red-600 hover:bg-red-700;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
{#if streamData}
|
{#if streamData}
|
||||||
{#if streamData.type === "Image"}
|
{#if streamData.type === "Image"}
|
||||||
<ImageViewer img={streamData.imageData} {height}
|
<ImageViewer imgUrl={streamData.imageData} {height}
|
||||||
></ImageViewer>
|
></ImageViewer>
|
||||||
{:else if streamData.textData}
|
{:else if streamData.textData}
|
||||||
<StreamEditor
|
<StreamEditor
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { UploadOutline } from "flowbite-svelte-icons";
|
|
||||||
import * as monaco from "monaco-editor";
|
import * as monaco from "monaco-editor";
|
||||||
|
|
||||||
// Define a custom theme for PDF content stream highlighting
|
// Define a custom theme for PDF content stream highlighting
|
||||||
@ -46,7 +45,7 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = monaco.editor.create(editorContainer, {
|
editor = monaco.editor.create(editorContainer, {
|
||||||
value: "",
|
value: "",
|
||||||
language: 'pdf-content-stream',
|
language: 'plaintext',
|
||||||
theme: "forge-dark",
|
theme: "forge-dark",
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
|
|||||||
@ -4,8 +4,7 @@
|
|||||||
let {upload} = $props();
|
let {upload} = $props();
|
||||||
let result_message = $state("");
|
let result_message = $state("");
|
||||||
</script>
|
</script>
|
||||||
|
<div class="w-full h-[20%]"></div>
|
||||||
<h1 class="text-xl m-5">Welcome to PDF Forge!</h1>
|
|
||||||
<div class="row" on:click={upload}>
|
<div class="row" on:click={upload}>
|
||||||
<img src="/pdf-forge-logo-bg.png" class="logo forge" alt="PDF Forge Logo"/>
|
<img src="/pdf-forge-logo-bg.png" class="logo forge" alt="PDF Forge Logo"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
onZoomIn: () => void;
|
onZoomIn: () => void;
|
||||||
onZoomOut: () => void;
|
onZoomOut: () => void;
|
||||||
resetZoom: () => void;
|
resetZoom: () => void;
|
||||||
fit: () => void;
|
fit?: () => void;
|
||||||
showFit: boolean;
|
showFit: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import type { OperatorList } from './OperatorList';
|
import type { OperatorList } from './OperatorList';
|
||||||
|
|
||||||
export default class ContentModel {
|
export default class ContentModel {
|
||||||
|
pageNum: number;
|
||||||
content: string;
|
content: string;
|
||||||
opList: OperatorList;
|
opList: OperatorList;
|
||||||
|
|
||||||
constructor(content: string, opList: OperatorList) {
|
constructor(pageNum: number, content: string, opList: OperatorList) {
|
||||||
|
this.pageNum = pageNum;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.opList = opList;
|
this.opList = opList;
|
||||||
}
|
}
|
||||||
@ -17,7 +19,7 @@ export default class ContentModel {
|
|||||||
for (let i = 0; i < this.opList.rangeArray.length; i++){
|
for (let i = 0; i < this.opList.rangeArray.length; i++){
|
||||||
const oldRange: number[] = this.opList.rangeArray[i];
|
const oldRange: number[] = this.opList.rangeArray[i];
|
||||||
if (!oldRange) {
|
if (!oldRange) {
|
||||||
newRanges.push([0, 0]);
|
newRanges.push(null);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
newRanges.push([result.lookup[oldRange[0]], result.lookup[oldRange[1]]]);
|
newRanges.push([result.lookup[oldRange[0]], result.lookup[oldRange[1]]]);
|
||||||
|
|||||||
@ -5,21 +5,27 @@ import type { TreeViewModel } from './TreeViewModel';
|
|||||||
import type XRefTable from './XRefTable';
|
import type XRefTable from './XRefTable';
|
||||||
import ContentModel from './ContentModel.svelte';
|
import ContentModel from './ContentModel.svelte';
|
||||||
import { OperatorList } from './OperatorList';
|
import { OperatorList } from './OperatorList';
|
||||||
|
import { StepperManager } from './Stepper.svelte';
|
||||||
|
|
||||||
export default class DocumentWorker {
|
export default class DocumentWorker {
|
||||||
|
|
||||||
doc: PDFDocumentProxy;
|
doc: PDFDocumentProxy;
|
||||||
|
stepperManager: StepperManager;
|
||||||
|
renderingTask: { promise: Promise<void>; cancel: () => void } | null;
|
||||||
|
|
||||||
constructor(doc: PDFDocumentProxy) {
|
constructor(doc: PDFDocumentProxy, stepperManager: StepperManager) {
|
||||||
this.doc = doc;
|
this.doc = doc;
|
||||||
|
this.renderingTask = null;
|
||||||
|
this.stepperManager = stepperManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async load(data: string | ArrayBuffer) {
|
static async load(data: string | ArrayBuffer) {
|
||||||
console.log("Initializing pdf worker")
|
|
||||||
GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
||||||
const loadingTask = getDocument({ data });
|
const stepperManager = new StepperManager();
|
||||||
|
// @ts-expect-error interface to untyped js
|
||||||
|
globalThis.StepperManager = stepperManager;
|
||||||
|
const loadingTask = getDocument({ data, pdfBug: true });
|
||||||
const doc = await loadingTask.promise;
|
const doc = await loadingTask.promise;
|
||||||
return new DocumentWorker(doc);
|
return new DocumentWorker(doc, stepperManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async save() {
|
public async save() {
|
||||||
@ -28,14 +34,13 @@ export default class DocumentWorker {
|
|||||||
|
|
||||||
public async getTitle(): Promise<string | undefined> {
|
public async getTitle(): Promise<string | undefined> {
|
||||||
let title = this.doc._pdfInfo.Title;
|
let title = this.doc._pdfInfo.Title;
|
||||||
console.log(this.doc._pdfInfo);
|
if (title && title !== '') {
|
||||||
if (title && title !== "") {
|
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
const metadata = await this.doc.getMetadata()
|
const metadata = await this.doc.getMetadata();
|
||||||
|
|
||||||
title = metadata?.info?.Title;
|
title = metadata?.info?.Title;
|
||||||
return title === "" ? undefined : title;
|
return title === '' ? undefined : title;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNumberOfPages(): number {
|
public getNumberOfPages(): number {
|
||||||
@ -47,12 +52,10 @@ export default class DocumentWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getPrimTreeByPath(request: TreeViewRequest[]): Promise<TreeViewModel[]> {
|
public async getPrimTreeByPath(request: TreeViewRequest[]): Promise<TreeViewModel[]> {
|
||||||
|
|
||||||
return await this.doc.getPrimitiveTree(request);
|
return await this.doc.getPrimitiveTree(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getXrefEntries(): Promise<XRefTable> {
|
public async getXrefEntries(): Promise<XRefTable> {
|
||||||
|
|
||||||
return await this.doc.getXRefEntries();
|
return await this.doc.getXRefEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,16 +67,23 @@ export default class DocumentWorker {
|
|||||||
try {
|
try {
|
||||||
const page = await this.doc.getPage(pageNumber);
|
const page = await this.doc.getPage(pageNumber);
|
||||||
const contentsPromise = page.getContents();
|
const contentsPromise = page.getContents();
|
||||||
const operatorList = await page.getOperatorList(pageNumber)
|
const operatorList = await page.getOperatorList(pageNumber);
|
||||||
const opList = OperatorList.createUnfoldedOpList(operatorList.fnArray, operatorList.argsArray, operatorList.rangeArray);
|
const opList = OperatorList.createUnfoldedOpList(
|
||||||
return new ContentModel(await contentsPromise, opList);
|
operatorList.fnArray,
|
||||||
|
operatorList.argsArray,
|
||||||
|
operatorList.rangeArray
|
||||||
|
);
|
||||||
|
return new ContentModel(pageNumber, await contentsPromise, opList);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return new ContentModel("", new OperatorList([], [], []));
|
return new ContentModel(pageNumber, '', new OperatorList([], [], [], []));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateContents(pageNumber: number, newContents: string) {
|
public async updateContents(pageNumber: number, newContents: string) {
|
||||||
|
if (this.renderingTask) {
|
||||||
|
await this.renderingTask.promise;
|
||||||
|
}
|
||||||
const page = await this.doc.getPage(pageNumber);
|
const page = await this.doc.getPage(pageNumber);
|
||||||
await page.updateContents(newContents);
|
await page.updateContents(newContents);
|
||||||
}
|
}
|
||||||
@ -83,7 +93,6 @@ export default class DocumentWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getImageAsUrl(path: string): Promise<string> {
|
public async getImageAsUrl(path: string): Promise<string> {
|
||||||
|
|
||||||
const imageData = await this.doc.getImageDataByPath(path);
|
const imageData = await this.doc.getImageDataByPath(path);
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@ -106,29 +115,40 @@ export default class DocumentWorker {
|
|||||||
return await page.getOperatorList();
|
return await page.getOperatorList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async renderPage(num: number, canvas: HTMLCanvasElement) {
|
public async renderPage(
|
||||||
|
num: number,
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
scale: number
|
||||||
|
) {
|
||||||
|
if (this.renderingTask) {
|
||||||
|
this.renderingTask.cancel();
|
||||||
|
this.renderingTask = null;
|
||||||
|
}
|
||||||
const page = await this.doc.getPage(num);
|
const page = await this.doc.getPage(num);
|
||||||
const scale = 1.5;
|
|
||||||
const viewport = page.getViewport({ scale: scale, });
|
const viewport = page.getViewport({ scale: scale });
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.style.width = Math.floor(canvas.width) + "px";
|
canvas.style.width = Math.floor(canvas.width) + 'px';
|
||||||
canvas.style.height = Math.floor(canvas.height) + "px";
|
canvas.style.height = Math.floor(canvas.height) + 'px';
|
||||||
const context = canvas.getContext("2d");
|
const context = canvas.getContext('2d');
|
||||||
if (!context) return "";
|
if (!context) return '';
|
||||||
const transform = [1, 0, 0, 1, 0, 0];
|
const transform = [1, 0, 0, 1, 0, 0];
|
||||||
// viewport = page.getViewport({ scale: 0.7, offsetX: canvas.width / 4, offsetY: canvas.height / 4});
|
|
||||||
|
|
||||||
const renderContext = {
|
const renderContext = {
|
||||||
canvasContext: context,
|
canvasContext: context,
|
||||||
transform: transform,
|
transform: transform,
|
||||||
viewport: viewport,
|
viewport: viewport
|
||||||
};
|
};
|
||||||
const renderTask = page.render(renderContext);
|
this.renderingTask = page.render(renderContext);
|
||||||
|
|
||||||
context.fillStyle = '#ffffff';
|
context.fillStyle = '#ffffff';
|
||||||
context.fillRect(viewport.offsetX, viewport.offsetY, viewport.width, viewport.height);
|
context.fillRect(viewport.offsetX, viewport.offsetY, viewport.width, viewport.height);
|
||||||
await renderTask.promise
|
await this.renderingTask?.promise;
|
||||||
|
this.renderingTask = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getPage(pageNum: number) {
|
||||||
|
return await this.doc.getPage(pageNum);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,63 +7,33 @@ export interface PDFOperator {
|
|||||||
class: string;
|
class: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OperatorList {
|
export interface Range {
|
||||||
fnArray: number[];
|
start: number;
|
||||||
argsArray: [];
|
end: number;
|
||||||
rangeArray: number[][];
|
}
|
||||||
constructor(fnArray: [], argsArray: [], rangeArray: []) {
|
|
||||||
this.fnArray = fnArray;
|
export class Operator {
|
||||||
this.argsArray = argsArray;
|
fnId: keyof typeof opLookup;
|
||||||
this.rangeArray = rangeArray;
|
args: [] | null;
|
||||||
|
range: Range | null;
|
||||||
|
ogIdx: number;
|
||||||
|
|
||||||
|
constructor(fnId: number, args: [] | null, range: Range | null, ogIdx: number) {
|
||||||
|
// @ts-expect-error can not happen
|
||||||
|
this.fnId = fnId;
|
||||||
|
this.args = args;
|
||||||
|
this.range = range;
|
||||||
|
this.ogIdx = ogIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setRangeArray(rangeArray: number[][]) {
|
getPDFOperator(): PDFOperator {
|
||||||
this.rangeArray = rangeArray;
|
return opLookup[this.fnId];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createUnfoldedOpList(fnArray: [], argsArray: [], rangeArray: []) {
|
toMarkdown() {
|
||||||
const unfoldedFnArray = [];
|
const operator = this.getPDFOperator();
|
||||||
const unfoldedArgsArray = [];
|
|
||||||
const unfoldedRangeArray = [];
|
|
||||||
for (let i = 0; i < fnArray.length; i++) {
|
|
||||||
unfoldedFnArray.push(fnArray[i]);
|
|
||||||
unfoldedArgsArray.push(argsArray[i]);
|
|
||||||
unfoldedRangeArray.push(rangeArray[i]);
|
|
||||||
if (fnArray[i] == 91) {
|
|
||||||
const [subFnArray, subArgsArray, subRangeArray] = OperatorList.unfoldConstructPath(
|
|
||||||
argsArray[i]
|
|
||||||
);
|
|
||||||
unfoldedFnArray.push(...subFnArray);
|
|
||||||
unfoldedArgsArray.push(...subArgsArray);
|
|
||||||
unfoldedRangeArray.push(...subRangeArray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new OperatorList(unfoldedFnArray, unfoldedArgsArray, unfoldedRangeArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unfoldConstructPath(args: [[], [], [], []]) {
|
|
||||||
const [subFnArray, subArgs, , subRanges] = args;
|
|
||||||
const subArgsArray = [];
|
|
||||||
const subRangesArray = [];
|
|
||||||
let argsOffset = 0;
|
|
||||||
for (let i = 0; i < subFnArray.length; i++) {
|
|
||||||
const op = getPDFOperator(subFnArray[i]);
|
|
||||||
const rangeOffset = i * 2;
|
|
||||||
const range = subRanges.slice(rangeOffset, rangeOffset + 2);
|
|
||||||
subRangesArray.push(range);
|
|
||||||
const newSubArgs = [];
|
|
||||||
for (let j = 0; j < (op.numArgs ?? 0); j++) {
|
|
||||||
newSubArgs.push(subArgs[argsOffset++]);
|
|
||||||
}
|
|
||||||
subArgsArray.push(newSubArgs);
|
|
||||||
}
|
|
||||||
return [subFnArray, subArgsArray, subRangesArray];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static formatOperatorToMarkdown(operatorId: number, args: any): string {
|
|
||||||
const operator = opLookup[operatorId] as PDFOperator;
|
|
||||||
if (!operator) {
|
if (!operator) {
|
||||||
return `Unknown operator: ${operatorId}`;
|
return `Unknown operator: ${this.fnId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyword = operator.keyword ? `\`${operator.keyword}\`` : 'N/A';
|
const keyword = operator.keyword ? `\`${operator.keyword}\`` : 'N/A';
|
||||||
@ -71,14 +41,18 @@ export class OperatorList {
|
|||||||
operator.numArgs
|
operator.numArgs
|
||||||
? `Arguments: ${operator.numArgs}${operator.variableArgs ? ' (variable)' : ''}`
|
? `Arguments: ${operator.numArgs}${operator.variableArgs ? ' (variable)' : ''}`
|
||||||
: 'No arguments';
|
: 'No arguments';
|
||||||
if (operator.keyword === "Tj" || operator.keyword === "TJ" && Array.isArray(args)) {
|
if ((operator.keyword === "Tj" || operator.keyword === "TJ") && Array.isArray(this.args)) {
|
||||||
let content = '';
|
let content = '';
|
||||||
content += "`"
|
content += "`"
|
||||||
for (const arg of args[0]) {
|
const textArgs = this.args.at(0)
|
||||||
|
if (textArgs && Array.isArray(textArgs)) {
|
||||||
|
// @ts-expect-error I don't know how to properly type this
|
||||||
|
for (const arg of textArgs) {
|
||||||
if (arg.unicode) {
|
if (arg.unicode) {
|
||||||
content += arg.unicode;
|
content += arg.unicode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
content += "`"
|
content += "`"
|
||||||
return `**${operator.name}** (${keyword})
|
return `**${operator.name}** (${keyword})
|
||||||
${operator.description}:
|
${operator.description}:
|
||||||
@ -91,7 +65,110 @@ export class OperatorList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OperatorListModel {
|
||||||
|
fnArray: number[];
|
||||||
|
argsArray: [];
|
||||||
|
rangeArray: number[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorList {
|
||||||
|
|
||||||
|
fnArray: number[];
|
||||||
|
argsArray: [];
|
||||||
|
rangeArray: number[][];
|
||||||
|
ogIdx: number[];
|
||||||
|
|
||||||
|
constructor(fnArray: [], argsArray: [], rangeArray: [], ogIdx: number[]) {
|
||||||
|
this.fnArray = fnArray;
|
||||||
|
this.argsArray = argsArray;
|
||||||
|
this.rangeArray = rangeArray;
|
||||||
|
this.ogIdx = ogIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public length() {
|
||||||
|
return this.fnArray.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ogLength() {
|
||||||
|
const l = this.ogIdx.at(-1);
|
||||||
|
if (l === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return l + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setRangeArray(rangeArray: number[][]) {
|
||||||
|
this.rangeArray = rangeArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromModel(opList: OperatorListModel): OperatorList {
|
||||||
|
// @ts-expect-error impossibly difficult to type
|
||||||
|
return this.createUnfoldedOpList(opList.fnArray, opList.argsArray, opList.rangeArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createUnfoldedOpList(fnArray: [], argsArray: [], rangeArray: []) {
|
||||||
|
const unfoldedFnArray = [];
|
||||||
|
const unfoldedArgsArray = [];
|
||||||
|
const unfoldedRangeArray = [];
|
||||||
|
const ogIdxArray = [];
|
||||||
|
for (let i = 0; i < fnArray.length; i++) {
|
||||||
|
if (fnArray[i] == 91) {
|
||||||
|
const [subFnArray, subArgsArray, subRangeArray] = OperatorList.unfoldConstructPath(
|
||||||
|
argsArray[i]
|
||||||
|
);
|
||||||
|
unfoldedFnArray.push(...subFnArray);
|
||||||
|
unfoldedArgsArray.push(...subArgsArray);
|
||||||
|
unfoldedRangeArray.push(...subRangeArray);
|
||||||
|
for (let j = 0; j < subFnArray.length; j++) {
|
||||||
|
ogIdxArray.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unfoldedFnArray.push(fnArray[i]);
|
||||||
|
unfoldedArgsArray.push(argsArray[i]);
|
||||||
|
unfoldedRangeArray.push(rangeArray[i]);
|
||||||
|
ogIdxArray.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// @ts-expect-error I don't know how to properly type this
|
||||||
|
return new OperatorList(unfoldedFnArray, unfoldedArgsArray, unfoldedRangeArray, ogIdxArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unfoldConstructPath(args: [[], [], [], []]) {
|
||||||
|
const [subFnArray, subArgs, , subRanges] = args;
|
||||||
|
const subArgsArray = [];
|
||||||
|
const subRangesArray = [];
|
||||||
|
let argsOffset = 0;
|
||||||
|
for (let i = 0; i < subFnArray.length; i++) {
|
||||||
|
const op = getPDFOperator(subFnArray[i]);
|
||||||
|
const rangeOffset = i * 2;
|
||||||
|
const range = subRanges.slice(rangeOffset, rangeOffset + 2);
|
||||||
|
subRangesArray.push(range);
|
||||||
|
// @ts-expect-error I don't know how to properly type this
|
||||||
|
const newSubArgs = [];
|
||||||
|
for (let j = 0; j < (op.numArgs ?? 0); j++) {
|
||||||
|
newSubArgs.push(subArgs[argsOffset++]);
|
||||||
|
}
|
||||||
|
// @ts-expect-error I don't know how to properly type this
|
||||||
|
subArgsArray.push(newSubArgs);
|
||||||
|
}
|
||||||
|
return [subFnArray, subArgsArray, subRangesArray];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOperatorAt(i: number) {
|
||||||
|
const fnId = this.fnArray.at(i);
|
||||||
|
const args = this.argsArray.at(i) ?? null;
|
||||||
|
const range = this.rangeArray.at(i);
|
||||||
|
const ogIdx = this.ogIdx.at(i);
|
||||||
|
if (fnId && ogIdx !== undefined) {
|
||||||
|
return new Operator(fnId, args, Array.isArray(range) ? {start: range[0], end: range[1]} : null, ogIdx);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Operator List Index not found at ${i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getPDFOperator(id: number | string): PDFOperator {
|
export function getPDFOperator(id: number | string): PDFOperator {
|
||||||
|
// @ts-expect-error I don't know how to properly type this
|
||||||
return opLookup[id];
|
return opLookup[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
src/models/PageRenderState.svelte.ts
Normal file
45
src/models/PageRenderState.svelte.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import type DocumentWorker from './Document.svelte';
|
||||||
|
import type FileViewState from './FileViewState.svelte';
|
||||||
|
|
||||||
|
export class PageRenderState {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
doc: DocumentWorker;
|
||||||
|
fState: FileViewState;
|
||||||
|
|
||||||
|
pageNum: number | undefined;
|
||||||
|
width: number | undefined;
|
||||||
|
height: number | undefined;
|
||||||
|
scale: number = $state(1);
|
||||||
|
|
||||||
|
constructor(fState: FileViewState, canvas: HTMLCanvasElement) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.doc = fState.document;
|
||||||
|
this.fState = fState;
|
||||||
|
this.scale = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePage(pageNum: number) {
|
||||||
|
if (pageNum === this.pageNum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await this.doc.getPage(pageNum);
|
||||||
|
const viewPort = page.getViewport({ scale: 1, });
|
||||||
|
this.pageNum = pageNum;
|
||||||
|
this.width = viewPort.width;
|
||||||
|
this.height = viewPort.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScale(scale: number) {
|
||||||
|
|
||||||
|
this.scale = scale;
|
||||||
|
return this.renderFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async renderFrame() {
|
||||||
|
if (this.fState.currentPageNumber) {
|
||||||
|
await this.doc.renderPage(this.fState.currentPageNumber, this.canvas, this.scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
90
src/models/Stepper.svelte.ts
Normal file
90
src/models/Stepper.svelte.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import type { OperatorListModel } from './OperatorList';
|
||||||
|
|
||||||
|
export class DebugStepper {
|
||||||
|
breakPoints: number[];
|
||||||
|
nextBreakPoint: number | null = null;
|
||||||
|
currentIdx: number = $state(0);
|
||||||
|
pageNum: number = 0;
|
||||||
|
onBreak: boolean = $state(false);
|
||||||
|
opList: OperatorListModel | null = null;
|
||||||
|
callback: (() => void) | null = null;
|
||||||
|
|
||||||
|
constructor(pageNum: number, breakPoints: Set<number>) {
|
||||||
|
this.pageNum = pageNum;
|
||||||
|
this.breakPoints = Array.from(breakPoints).sort(function (a, b) {
|
||||||
|
return b - a;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBreakPoints(breakPoints: Set<number>) {
|
||||||
|
this.breakPoints = Array.from(breakPoints).filter(a => a > this.currentIdx).sort(function (a, b) {
|
||||||
|
return b - a;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async breakIt(idx: number, callback: () => void) {
|
||||||
|
this.onBreak = true;
|
||||||
|
this.currentIdx = idx;
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public continue() {
|
||||||
|
this.nextBreakPoint = this.popNextBreakPoint();
|
||||||
|
if (this.callback) {
|
||||||
|
this.callback();
|
||||||
|
}
|
||||||
|
this.onBreak = false;
|
||||||
|
this.callback = null;
|
||||||
|
this._resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
public step() {
|
||||||
|
this.nextBreakPoint = this.currentIdx + 1;
|
||||||
|
this._resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resume() {
|
||||||
|
if (this.callback) {
|
||||||
|
this.callback();
|
||||||
|
}
|
||||||
|
this.onBreak = false;
|
||||||
|
this.callback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNextBreakPoint() {
|
||||||
|
|
||||||
|
return this.nextBreakPoint ?? this.breakPoints.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private popNextBreakPoint() {
|
||||||
|
return this.breakPoints.pop() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(operatorList: OperatorListModel) {
|
||||||
|
this.opList = operatorList;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOperatorList(operatorList: OperatorListModel) {
|
||||||
|
this.opList = operatorList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StepperManager {
|
||||||
|
stepper: DebugStepper | null = null;
|
||||||
|
enabled: boolean = false;
|
||||||
|
|
||||||
|
register(stepper: DebugStepper) {
|
||||||
|
this.stepper = stepper;
|
||||||
|
this.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister(stepper: DebugStepper) {
|
||||||
|
this.stepper = null;
|
||||||
|
this.enabled = false;
|
||||||
|
}
|
||||||
|
// must be called like this for api reasons
|
||||||
|
create(pageNum: number) {
|
||||||
|
return this.stepper;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/models/ViewState.ts
Normal file
6
src/models/ViewState.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export type ViewState = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user