mirror of
https://github.com/poanse/obsidian-taskmap.git
synced 2026-07-22 06:05:58 +00:00
implemented lines
This commit is contained in:
parent
86125b1944
commit
ab1b3bd767
6 changed files with 133 additions and 31 deletions
|
|
@ -1,7 +1,7 @@
|
|||
export const BUTTON_SIZE = 18 * 2;
|
||||
export const TOOLBAR_GAP = 2;
|
||||
export const TOOLBAR_PADDING = { x: 4, y: 4 };
|
||||
export const TOOLBAR_SHIFT = 4 * 2;
|
||||
export const TOOLBAR_SHIFT = 4;
|
||||
export const SUBTOOLBAR_SHIFT = 2;
|
||||
|
||||
export const TOOLBAR_SIZE = {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// Assumed types and constants based on usage
|
||||
import type { TaskData, TaskId, Vector2 } from "./types";
|
||||
|
||||
export const NoNodeId = -1 as TaskId;
|
||||
export const RootNodeId = 0 as TaskId;
|
||||
export const NoTaskId = -1 as TaskId;
|
||||
export const RootTaskId = 0 as TaskId;
|
||||
|
||||
// Helper for Vector2 operations since TS doesn't support operator overloading
|
||||
const V2 = {
|
||||
export const V2 = {
|
||||
Zero: { x: 0, y: 0 },
|
||||
One: { x: 1, y: 1 },
|
||||
add: (v1: Vector2, v2: Vector2): Vector2 => ({
|
||||
|
|
@ -65,7 +65,7 @@ export class NodePositionsCalculator {
|
|||
const parentFramePositions =
|
||||
this.CalculatePositionsInParentFrame(tasks);
|
||||
const positions: Map<TaskId, Vector2> = new Map<TaskId, Vector2>();
|
||||
positions.set(NoNodeId, V2.Zero);
|
||||
positions.set(NoTaskId, V2.Zero);
|
||||
|
||||
tasks.forEach((t) => {
|
||||
const parentPos = positions.get(t.parentId)!;
|
||||
|
|
@ -125,7 +125,7 @@ export class NodePositionsCalculator {
|
|||
Vector2
|
||||
>();
|
||||
|
||||
const allIdsToProcess = [...sortedTasks.map((t) => t.taskId), NoNodeId];
|
||||
const allIdsToProcess = [...sortedTasks.map((t) => t.taskId), NoTaskId];
|
||||
|
||||
allIdsToProcess.forEach((id) => {
|
||||
if (childrenIdsByParentId.has(id)) {
|
||||
|
|
@ -229,7 +229,7 @@ export class NodePositionsCalculator {
|
|||
});
|
||||
|
||||
const rootTaskHasMultipleVisibleChildren =
|
||||
(childrenIdsByParentId.get(RootNodeId)! || []).length > 1;
|
||||
(childrenIdsByParentId.get(RootTaskId)! || []).length > 1;
|
||||
|
||||
if (
|
||||
rootTaskHasMultipleVisibleChildren &&
|
||||
|
|
@ -296,7 +296,7 @@ export class NodePositionsCalculator {
|
|||
TaskId
|
||||
>;
|
||||
const rootChildren =
|
||||
childrenIdsByParentId.get(RootNodeId) ?? [];
|
||||
childrenIdsByParentId.get(RootTaskId) ?? [];
|
||||
rootChildren.forEach((id, idx) => {
|
||||
// reenumerate priorities based on index
|
||||
rootChildrenByPriority.set(idx, id);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { StatusCode, type TaskData, type TaskId } from "./types";
|
||||
import { NoNodeId, RootNodeId } from "./NodePositionsCalculator";
|
||||
import { NoTaskId, RootTaskId } from "./NodePositionsCalculator";
|
||||
|
||||
export class ProjectData {
|
||||
tasks = $state(new Array<TaskData>());
|
||||
curTaskId = RootNodeId;
|
||||
curTaskId = RootTaskId;
|
||||
|
||||
public static getDefault(): ProjectData {
|
||||
return new ProjectData({
|
||||
|
|
@ -31,7 +31,7 @@ export class ProjectData {
|
|||
public addRootTask() {
|
||||
this.tasks.push({
|
||||
taskId: this.curTaskId,
|
||||
parentId: NoNodeId,
|
||||
parentId: NoTaskId,
|
||||
status: StatusCode.IN_PROGRESS,
|
||||
name: "root",
|
||||
priority: 0,
|
||||
|
|
|
|||
31
src/components/Connection.svelte
Normal file
31
src/components/Connection.svelte
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import type {Vector2} from "../types";
|
||||
|
||||
let { startPoint, endPoint}: {startPoint: Vector2, endPoint: Vector2} = $props();
|
||||
|
||||
// Calculate the path string reactively
|
||||
let pathData = $derived.by(() => {
|
||||
const midX = (startPoint.x + endPoint.x) / 2;
|
||||
// M = Move to start
|
||||
// C = Cubic Bezier (ControlPoint1, ControlPoint2, EndPoint)
|
||||
return `M ${startPoint.x} ${startPoint.y} C ${midX} ${startPoint.y}, ${midX} ${endPoint.y}, ${endPoint.x} ${endPoint.y}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<path
|
||||
d={pathData}
|
||||
fill="transparent"
|
||||
stroke-linecap="round"
|
||||
vector-effect="non-scaling-stroke"
|
||||
shape-rendering="geometricPrecision"
|
||||
/>
|
||||
|
||||
<style>
|
||||
path {
|
||||
position: absolute;
|
||||
transition: stroke 0.2s;
|
||||
pointer-events: none; /* Let clicks pass through to nodes below */
|
||||
stroke: #555;
|
||||
stroke-width: 4;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -161,7 +161,9 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
.task-container {
|
||||
z-index: 3; /* over lines*/
|
||||
}
|
||||
.task {
|
||||
/*background: #111;*/
|
||||
/*background-color: #1E1E1E;*/
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@
|
|||
import Panzoom, {type PanzoomObject} from '@panzoom/panzoom'
|
||||
import type {Context} from "../Context.svelte.js";
|
||||
import {MouseDown} from "../types";
|
||||
import Connection from "./Connection.svelte";
|
||||
import {RootTaskId, V2} from "../NodePositionsCalculator";
|
||||
import {TASK_SIZE} from "../Constants";
|
||||
|
||||
let {context}: {context: Context} = $props();
|
||||
|
||||
let viewportEl: HTMLDivElement | null = null;
|
||||
let sceneEl: HTMLDivElement | null = null;
|
||||
let svgGroupEl: SVGGElement | null = null;
|
||||
let panzoom: PanzoomObject | null = null;
|
||||
let isDragging = false;
|
||||
let mouseDown = $state(MouseDown.NONE); // -1
|
||||
|
|
@ -17,10 +21,16 @@
|
|||
let startY = 0;
|
||||
let panStartX = 0;
|
||||
let panStartY = 0;
|
||||
// let cssTransform = $derived(`translate(${transformState.x}px, ${transformState.y}px) scale(${transformState.scale})`);
|
||||
// let svgTransform = $derived(`translate(${transformState.x} ${transformState.y}) scale(${transformState.scale})`);
|
||||
|
||||
|
||||
onMount(async () => {
|
||||
if (!sceneEl) {
|
||||
throw new Error('No mount element');
|
||||
throw new Error('No scene element');
|
||||
}
|
||||
if (!svgGroupEl) {
|
||||
throw new Error('No svg group element');
|
||||
}
|
||||
if (!viewportEl) {
|
||||
throw new Error('No viewport');
|
||||
|
|
@ -122,20 +132,32 @@
|
|||
{onpointerdown}
|
||||
{onpointermove}
|
||||
{onpointerup}
|
||||
style="width: 100%; height: 100vh; overflow: hidden; position: relative; background: #1C1C1C;"
|
||||
>
|
||||
<div
|
||||
class="task-container"
|
||||
class="scene"
|
||||
bind:this={sceneEl}
|
||||
tabindex="-1"
|
||||
onkeydown={handleKey}
|
||||
style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;"
|
||||
role="presentation"
|
||||
>
|
||||
{#each context.projectData.tasks.filter(t => !t.deleted) as task}
|
||||
<Task taskId={task.taskId} {context} coords={context.getCurrentTaskPosition(task.taskId)} />
|
||||
{/each}
|
||||
<svg class="svg-layer" overflow="visible">
|
||||
<g class="svg-group" bind:this={svgGroupEl}>
|
||||
{#each context.projectData.tasks.filter(t => !t.deleted).filter(t => t.taskId !== RootTaskId) as task (task.taskId)}
|
||||
<Connection
|
||||
startPoint={V2.add(context.getCurrentTaskPosition(task.parentId), {x: TASK_SIZE.width, y: TASK_SIZE.height/2})}
|
||||
endPoint={V2.add(context.getCurrentTaskPosition(task.taskId), {x: 0, y: TASK_SIZE.height/2})}
|
||||
/>
|
||||
{/each}
|
||||
</g>
|
||||
</svg>
|
||||
<div
|
||||
class="task-layer"
|
||||
tabindex="-1"
|
||||
onkeydown={handleKey}
|
||||
role="presentation"
|
||||
>
|
||||
{#each context.projectData.tasks.filter(t => !t.deleted) as task (task.taskId)}
|
||||
<Task taskId={task.taskId} {context} coords={context.getCurrentTaskPosition(task.taskId)}/>
|
||||
{/each}
|
||||
|
||||
</div>
|
||||
{#if context.selectedTaskId !== -1}
|
||||
<Toolbar
|
||||
context={context}
|
||||
|
|
@ -145,17 +167,12 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.task-container {
|
||||
.viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
|
||||
background-color: #1C1C1C;
|
||||
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
}
|
||||
.viewport {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #1C1C1C;
|
||||
touch-action: none; /* Prevents mobile browser interference */
|
||||
}
|
||||
.viewport.is-panning {
|
||||
|
|
@ -164,4 +181,56 @@
|
|||
.viewport:not(.is-panning) {
|
||||
cursor: default;
|
||||
}
|
||||
.scene {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
/* Do NOT use flex centering here; it breaks coordinate math */
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
.svg-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.svg-group {
|
||||
display: flex;
|
||||
transform-style: preserve-3d;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.task-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/*transform-origin: 0;*/
|
||||
/*align-items: center;*/
|
||||
/*justify-content: center;*/
|
||||
/*background-color: #1C1C1C;*/
|
||||
/* The 0.1deg rotation is invisible but forces high-res redraws */
|
||||
/*perspective: 1000px;*/
|
||||
/*transform: scale(var(--scale)) rotateX(0.1deg);*/
|
||||
will-change: transform;
|
||||
|
||||
/* Prevents the browser from creating a low-res snapshot */
|
||||
/*backface-visibility: visible;*/
|
||||
|
||||
/* Forces the browser to keep the vector data sharp */
|
||||
/*transform-style: preserve-3d;*/
|
||||
|
||||
/* Ensure no blur filters are accidentally applied */
|
||||
/*filter: blur(0);*/
|
||||
}
|
||||
/* Ensure Tasks themselves have pointer-events: auto */
|
||||
:global(.task-layer > *) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue