implemented lines

This commit is contained in:
poanse 2025-12-20 05:01:48 +03:00
parent 86125b1944
commit ab1b3bd767
6 changed files with 133 additions and 31 deletions

View file

@ -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 = {

View file

@ -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);

View file

@ -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,

View 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>

View file

@ -161,7 +161,9 @@
{/if}
<style>
.task-container {
z-index: 3; /* over lines*/
}
.task {
/*background: #111;*/
/*background-color: #1E1E1E;*/

View file

@ -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>