Merge branch 'master' of https://gitee.com/zzzisfunnyboy/obsidian-notes
This commit is contained in:
104
.obsidian/workspace.json
vendored
104
.obsidian/workspace.json
vendored
@@ -4,16 +4,16 @@
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "023535fd6fa2e67d",
|
||||
"id": "1538f6ad5b15def5",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "c3a1fa114f02064b",
|
||||
"id": "afc535d48add9e90",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "skills/obsidian-auto-cards/SKILL.md",
|
||||
"file": "2Areas/游戏开发/pcei速度预估.md",
|
||||
"mode": "source",
|
||||
"source": false,
|
||||
"backlinks": true,
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "SKILL"
|
||||
"title": "pcei速度预估"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -63,7 +63,7 @@
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "tag:#flashcards/政治/近代史",
|
||||
"query": "",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
@@ -74,16 +74,6 @@
|
||||
"title": "搜索"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8f2786b27c7d1630",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "starred",
|
||||
"state": {},
|
||||
"icon": "lucide-ghost",
|
||||
"title": "starred"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8c8dfe109a7f116f",
|
||||
"type": "leaf",
|
||||
@@ -107,8 +97,7 @@
|
||||
"title": "文件列表"
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentTab": 4
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
@@ -147,8 +136,8 @@
|
||||
"state": {
|
||||
"type": "calendar",
|
||||
"state": {},
|
||||
"icon": "calendar-with-checkmark",
|
||||
"title": "Calendar"
|
||||
"icon": "lucide-ghost",
|
||||
"title": "calendar"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -210,12 +199,11 @@
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 200,
|
||||
"collapsed": true
|
||||
"width": 200
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"bases:新建数据库": false,
|
||||
"bases:创建新数据库": false,
|
||||
"templates:插入模板": false,
|
||||
"canvas:新建白板": false,
|
||||
"switcher:打开快速切换": true,
|
||||
@@ -224,46 +212,50 @@
|
||||
"copilot:Open Copilot Chat": false
|
||||
}
|
||||
},
|
||||
"active": "c3a1fa114f02064b",
|
||||
"active": "f2ac97a7addb8678",
|
||||
"lastOpenFiles": [
|
||||
"1Project/图文合批/图文使用不同的Tex2DArr.md",
|
||||
"skills/obsidian-auto-cards/SKILL.md",
|
||||
"skills/obsidian-auto-cards",
|
||||
"1Project/图文合批/图文合批-目录.md",
|
||||
"2Areas/游戏开发/索引.md",
|
||||
"skills/obsidian-markdown/SKILL.md",
|
||||
"skills/obsidian-bases/SKILL.md",
|
||||
"skills/json-canvas/SKILL.md",
|
||||
"skills/obsidian-markdown",
|
||||
"skills/obsidian-bases",
|
||||
"skills/json-canvas",
|
||||
"skills",
|
||||
"未命名.md",
|
||||
"2Areas/游戏开发/pcei速度预估.md",
|
||||
"2Areas/游戏开发/物理碰撞/物理碰撞算法.md",
|
||||
"1Project/海量渲染战斗/GPU动画工程/GPU蒙皮动画.md",
|
||||
"2Areas/游戏开发/物理碰撞",
|
||||
"1Project/海量渲染战斗/GPU动画工程/GPU顶点动画.md",
|
||||
"1Project/海量渲染战斗/GPU动画工程/bindpose.md",
|
||||
"1Project/GPU地形/Houdini PCG.md",
|
||||
"1Project/GPU地形/GPU Driven Terrain 跟学与实现.md",
|
||||
"渲染/软渲染/图片/9702b7cbafa93d6e3d0cd59e7cf37e31.png",
|
||||
"渲染/软渲染/图片/86aa46af36c4c3b1f1efef2a1ccc6310.png",
|
||||
"渲染/软渲染/图片/62ad22d3e981954fc083018fb850413a.png",
|
||||
"渲染/软渲染/图片/23d61a2b7376ea3149d74d6bd4ea9430.png",
|
||||
"2Areas/年度复盘/2025.md",
|
||||
"1Project/ZYGame/州府前端流程.md",
|
||||
"1Project/海量渲染战斗/GPU动画工程/TODO转为Job组织数据.md",
|
||||
"1Project/海量渲染战斗/GPU动画工程/TODO将Time存进buffer中,做成不同的实例可以使用不同的动画时间.md",
|
||||
"1Project/海量渲染战斗/GPU动画工程/TODO不同动作存进Buffer中.md",
|
||||
"1Project/海量渲染战斗/GPU动画工程/TODO 不同动作间的混合.md",
|
||||
"1Project/海量渲染战斗/GPU动画.md",
|
||||
"1Project/海量渲染战斗/GPU动画工程",
|
||||
"1Project/海量渲染战斗/批量渲染gpu动画.md",
|
||||
"1Project/海量渲染战斗/基于GPU蒙皮动画的Spine实现.md",
|
||||
"1Project/海量渲染战斗/基于GPU动画和ComputeShader的大批量动画渲染Demo(一)之GPU顶点动画.md",
|
||||
"1Project/海量渲染战斗/海量渲染战斗-目录.md",
|
||||
"1Project/构建新框架/MVVM/MVVM-目录.md",
|
||||
"1Project/构建新框架/MVVM/欠缺内容.md",
|
||||
"1Project/项目-目录.md",
|
||||
"InBox/InBox-目录.md",
|
||||
"00-Home.md",
|
||||
"1Project/ZYGame",
|
||||
"1Project/GPU地形/ComputeShader生成无限世界地形.md",
|
||||
"2Areas/游戏开发/fgui&ugui合批记录.md",
|
||||
"1Project/图文合批/字体字形图片申请.md",
|
||||
"1Project/图文合批/图文使用不同的Tex2DArr.md",
|
||||
"1Project/UE",
|
||||
"1Project/海量渲染战斗",
|
||||
"1Project/GPU地形",
|
||||
"wallhaven-rq75r7.jpg",
|
||||
"1Project/租房讨回押金/租新房注意事项.md",
|
||||
"1Project/租房讨回押金/处理流程.md",
|
||||
"1Project/宠物宇宙/地图编辑器/流程组件.md",
|
||||
"3Resources/书本/毛选/第一章.md",
|
||||
"1Project/中原精英/RoadMap.md",
|
||||
"1Project/学习/GAS.md",
|
||||
"1Project/学习/开发文章/将开发文章阅读并分类.md",
|
||||
"1Project/宠物宇宙/版本需求.md",
|
||||
"1Project/宠物宇宙/地图编辑器/需要做的优化.md",
|
||||
"1Project/宠物宇宙/地图编辑器/需要支持的地图功能.md",
|
||||
"4Archives/宠物宇宙/用户卡及游戏信息处理过程设计文档.md",
|
||||
"1Project/宠物宇宙/皮肤导入.md",
|
||||
"1Project/框架搭建.md",
|
||||
"1Project/构建新框架/管道模式/未命名.canvas",
|
||||
"4Archives/宠物宇宙",
|
||||
"4Archives/AI for Obsidian/20250315 - 测试 Obsidian+ AI 的能力边界/第四个用法|成为对弈的棋手/正方观点/观点9.md.edtz",
|
||||
"4Archives/AI for Obsidian/20250315 - 测试 Obsidian+ AI 的能力边界/第四个用法|成为对弈的棋手/正方观点/观点8.md.edtz",
|
||||
"4Archives/AI for Obsidian/20250315 - 测试 Obsidian+ AI 的能力边界/第四个用法|成为对弈的棋手/正方观点/观点7.md.edtz",
|
||||
"4Archives/AI for Obsidian/20250315 - 测试 Obsidian+ AI 的能力边界/第四个用法|成为对弈的棋手/正方观点/观点6.md.edtz",
|
||||
"4Archives/AI for Obsidian/20250315 - 测试 Obsidian+ AI 的能力边界/第四个用法|成为对弈的棋手/正方观点/观点5.md.edtz",
|
||||
"4Archives/AI for Obsidian/20250315 - 测试 Obsidian+ AI 的能力边界/第四个用法|成为对弈的棋手/正方观点/观点4.md.edtz",
|
||||
"4Archives/AI for Obsidian/20250315 - 测试 Obsidian+ AI 的能力边界/第四个用法|成为对弈的棋手/正方观点/观点3.md.edtz",
|
||||
"4Archives/AI for Obsidian/20250315 - 测试 Obsidian+ AI 的能力边界/第四个用法|成为对弈的棋手/正方观点/观点2.md.edtz",
|
||||
"2Areas/如何记录笔记/P.A.R.A/Pasted image 20250614192838.png",
|
||||
"1Project/战斗编辑器/随想/动作时间轴 随想.canvas",
|
||||
"2Areas/开发感悟/管道模式/未命名.canvas",
|
||||
@@ -274,8 +266,6 @@
|
||||
"InBox/TODO.canvas",
|
||||
"渲染/软渲染/图片/Pasted image 20250609171235.png",
|
||||
"InBox/渲染/GPU/Pasted image 20230413163234.png",
|
||||
"InBox/Mesh/图片/Pasted image 20221021171104.png",
|
||||
"InBox/渲染/软渲染/图片/Pasted image 20230613102109.png",
|
||||
"未命名 1.canvas",
|
||||
"并行计算/GPU.canvas",
|
||||
"并行计算/CPU.canvas",
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# 宣战
|
||||
- 宣战权限
|
||||
- 时间
|
||||
- 可宣战时间 点数
|
||||
- 可配置
|
||||
- 不可宣战周期
|
||||
|
||||
# 同盟城池界面
|
||||
- 城池列表界面
|
||||
- 分配奖励功能-列表
|
||||
- 分配上限
|
||||
- 配置
|
||||
- 排序
|
||||
- 占领buff奖励列表界面
|
||||
- 分配记录界面
|
||||
|
||||
# 占领规则
|
||||
- 首战
|
||||
- 可以分配奖励
|
||||
- 正式占领
|
||||
- 超出可占领数量弹出放弃最低势力值提示
|
||||
- 放弃占领
|
||||
- **武侯**可以放弃
|
||||
- 一小时缓冲
|
||||
|
||||
# 攻防
|
||||
- 进攻:
|
||||
- 个人攻击
|
||||
- 主力
|
||||
- 城防
|
||||
- 集结攻击
|
||||
- 列表
|
||||
- 剩余时间
|
||||
- 占领排行界面
|
||||
- 等级限制(配置,加盟时间限制(配置,新增世界buff
|
||||
- 功勋商店
|
||||
- 防守
|
||||
- 列表
|
||||
- 互斥
|
||||
- 主力
|
||||
- 城防
|
||||
- 攻占UI
|
||||
- 首占
|
||||
- 最终占领
|
||||
- 占领结果
|
||||
- 排行榜界面
|
||||
- 保护
|
||||
- 不在本赛季开放
|
||||
- 非宣战时间
|
||||
|
||||
# 野城
|
||||
- 单人进攻提示
|
||||
@@ -22,13 +22,22 @@
|
||||
你的当前顶点动画管线可以直接作为“LOD 动画层”保留,不与 GPU Skinning 冲突,只要切换 Renderer/Material 即可。
|
||||
#### 蒙皮动画需要记录的数据有哪些?
|
||||
- 一张原始网格的Vector3数据贴图
|
||||
- 收集骨骼和骨骼索引
|
||||
- 重映射后的骨骼索引
|
||||
- 如果之前
|
||||
- 一个动作
|
||||
- 该动作中骨骼每帧的坐标旋转
|
||||
- 会在Shader中计算**混合骨骼**后的TRS * 原始网格的TRS
|
||||
-
|
||||
- 收集[[bindpose]]
|
||||
- 收集全局骨骼索引
|
||||
- 根据不同的蒙皮组件需要的骨骼,重新映射索引
|
||||
- 为什么需要这个?
|
||||
- 身体的骨骼索引,索引分别是1、2、3、4
|
||||
- 腰
|
||||
- 肩膀
|
||||
- 手臂
|
||||
- 手掌
|
||||
- 而手臂为,索引为3、4
|
||||
- 手臂
|
||||
- 手掌
|
||||
- 该动作中每个网格的顶点计算:
|
||||
- Root在世界空间坐标的TRS
|
||||
- 每根骨骼还原[[bindpose]]
|
||||
- **混合骨骼**后的TRS * 原始网格的TRS
|
||||
|
||||
#### 蒙皮动画不同部位索引的问题
|
||||
- 蒙皮的骨骼索引在cpu中无问题,在gpu中有问题
|
||||
|
||||
5
1Project/海量渲染战斗/GPU动画工程/bindpose.md
Normal file
5
1Project/海量渲染战斗/GPU动画工程/bindpose.md
Normal file
@@ -0,0 +1,5 @@
|
||||
动作文件在导入Unity后会将网格顶点绑定在**蒙皮节点空间**下的坐标储存在bindpose中。
|
||||
- 例如:如果根节点在(0,0,0),骨骼在(0,0,10),网格顶点在骨骼空间坐标为(0,0,1),在导入之后,顶点坐标会被保存为(0,0,11);
|
||||
|
||||
|
||||
- GPU蒙皮动作中,需要将顶点还原为**骨骼节点空间下**坐标后再重新从root开始算
|
||||
7
2Areas/游戏开发/物理碰撞/物理碰撞算法.md
Normal file
7
2Areas/游戏开发/物理碰撞/物理碰撞算法.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# AABB
|
||||
|
||||
# SAT 分离轴定理
|
||||
|
||||
# GJK 明可夫斯基差
|
||||
- 两个形状每个顶点两两相差得出新的形状,如果原点位于形状内,则有碰撞
|
||||
- 再次优化,有选择的相减,是否有最简模型
|
||||
100
InBox/20260211-180200-VContainer-Unity-DI-文档笔记.md
Normal file
100
InBox/20260211-180200-VContainer-Unity-DI-文档笔记.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# VContainer(Unity DI)文档笔记
|
||||
|
||||
来源: https://vcontainer.hadashikick.jp/
|
||||
记录时间: 2026-02-11
|
||||
|
||||
## 一句话总结
|
||||
|
||||
VContainer 是面向 Unity 的高性能 DI 容器,核心是 `LifetimeScope + 明确生命周期 + PlayerLoop EntryPoint`,兼顾性能、可维护性与可测试性。
|
||||
|
||||
## 核心概念
|
||||
|
||||
- `LifetimeScope` 是组合根(Composition Root),在这里集中做注册。
|
||||
- 依赖推荐通过构造函数注入,业务入口可以使用纯 C# 类,不必全部依赖 `MonoBehaviour`。
|
||||
- 容器不可变(Immutable),强调线程安全和稳定性。
|
||||
|
||||
## 安装方式
|
||||
|
||||
- OpenUPM(推荐)
|
||||
- 命令: `openupm add jp.hadashikick.vcontainer`
|
||||
- UPM Git URL
|
||||
- `"jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.17.0"`
|
||||
- 手动导入 `.unitypackage`
|
||||
- 官方要求: Unity 2018.4+
|
||||
|
||||
## Hello World 最小流程
|
||||
|
||||
1. 新建继承 `LifetimeScope` 的组件(如 `GameLifetimeScope`)。
|
||||
2. 在 `Configure(IContainerBuilder builder)` 中注册依赖。
|
||||
3. 把该 `LifetimeScope` 挂到场景中的 GameObject。
|
||||
4. 通过构造函数自动注入依赖。
|
||||
5. 需要接入 Unity 生命周期时,使用 marker interface(例如 `ITickable`)。
|
||||
|
||||
## 生命周期(Lifetime)
|
||||
|
||||
- `Singleton`
|
||||
- 全容器共享一个实例。
|
||||
- 同一容器内同类型不能重复注册。
|
||||
- `Transient`
|
||||
- 每次 `Resolve` 新建实例。
|
||||
- `Scoped`
|
||||
- 每个 `LifetimeScope` 一份实例。
|
||||
- 子 Scope 与父 Scope 可以拥有各自实例。
|
||||
- Scope 销毁时会释放引用并调用已注册对象的 `IDisposable`。
|
||||
|
||||
父子 Scope 规则要点:
|
||||
|
||||
- 子 Scope 找不到注册时会向父 Scope 查找。
|
||||
- 父子都注册了同类型时,优先使用“最近的 Scope”。
|
||||
- 注意:如果仅销毁 `LifetimeScope`,注册为 `Lifetime.Scoped` 的 `MonoBehaviour` 不会自动销毁;需要通过层级管理或显式释放策略处理。
|
||||
|
||||
## 常见注册写法
|
||||
|
||||
- 注册具体类
|
||||
- `builder.Register<ServiceA>(Lifetime.Singleton);`
|
||||
- 以接口注册
|
||||
- `builder.Register<IServiceA, ServiceA>();`
|
||||
- 多接口注册
|
||||
- `builder.Register<ServiceA>(Lifetime.Singleton).As<IServiceA, IInputPort>();`
|
||||
- 自动注册已实现接口
|
||||
- `builder.Register<ServiceA>(Lifetime.Singleton).AsImplementedInterfaces();`
|
||||
- 同时保留接口与自身类型
|
||||
- `builder.Register<ServiceA>(Lifetime.Singleton).AsImplementedInterfaces().AsSelf();`
|
||||
- 实例注册
|
||||
- `builder.RegisterInstance(obj);`
|
||||
- 注意:`RegisterInstance` 默认等同单例,但实例生命周期不由容器托管(不会自动 `Dispose`、不会自动做方法注入)。
|
||||
|
||||
## Plain C# EntryPoint(推荐实践)
|
||||
|
||||
- 使用 `builder.RegisterEntryPoint<T>()` 把纯 C# 类挂到 VContainer 自己的 PlayerLoop。
|
||||
- 可用接口与大致时机:
|
||||
- `IInitializable` / `IPostInitializable`
|
||||
- `IStartable` / `IAsyncStartable` / `IPostStartable`
|
||||
- `IFixedTickable` / `IPostFixedTickable`
|
||||
- `ITickable` / `IPostTickable`
|
||||
- `ILateTickable` / `IPostLateTickable`
|
||||
- `IDisposable`(随容器释放)
|
||||
- 未捕获异常可用 `RegisterEntryPointExceptionHandler` 自定义处理。
|
||||
|
||||
## 性能与优化
|
||||
|
||||
- 官方定位:相对 Zenject 在 Resolve 路径上更快、GC 更低。
|
||||
- 默认使用反射;可启用 Source Generator 提升运行时性能。
|
||||
- Source Generator 要求 Unity 2021.3+;从 v1.13.0 起基于 Roslyn Source Generator。
|
||||
|
||||
## 我的落地建议(Unity 项目)
|
||||
|
||||
- 把 `LifetimeScope` 当成“模块边界”,按场景/系统拆分父子 Scope。
|
||||
- 业务逻辑放纯 C#,`MonoBehaviour` 尽量薄,只做视图与桥接。
|
||||
- 高频路径优先构造函数注入,减少运行期反射与临时分配。
|
||||
- 先统一生命周期约定(Singleton/Scoped/Transient 选择标准),再大规模迁移。
|
||||
|
||||
## 参考页面
|
||||
|
||||
- About: https://vcontainer.hadashikick.jp/
|
||||
- Installation: https://vcontainer.hadashikick.jp/getting-started/installation
|
||||
- Hello World: https://vcontainer.hadashikick.jp/getting-started/hello-world
|
||||
- Register Plain C# Type: https://vcontainer.hadashikick.jp/registering/register-type
|
||||
- Lifetime Overview: https://vcontainer.hadashikick.jp/scoping/lifetime-overview
|
||||
- Plain C# Entry point: https://vcontainer.hadashikick.jp/integrations/entrypoint
|
||||
- Source Generator: https://vcontainer.hadashikick.jp/optimization/source-generator
|
||||
9
InBox/未看的帖子.md
Normal file
9
InBox/未看的帖子.md
Normal file
@@ -0,0 +1,9 @@
|
||||
- https://github.com/cinight/MinimalCompute/tree/6000.0 ComputeShader的学习
|
||||
- https://zhuanlan.zhihu.com/p/1976296247481619134 开源帧同步战斗系统
|
||||
|
||||
- https://zhuanlan.zhihu.com/p/1942212458711323177 性能优化
|
||||
- https://zhuanlan.zhihu.com/p/1942014668949414119
|
||||
- https://zhuanlan.zhihu.com/p/1941556075905160582
|
||||
- https://zhuanlan.zhihu.com/p/1962123844602016654
|
||||
- https://www.zhihu.com/question/31186642/answer/1918643987742430329?utm_campaign=shareopn&utm_medium=social&utm_psn=1957443514607596321&utm_source=wechat_session
|
||||
- https://blog.uwa4d.com/archives/BlueBook_2025.html
|
||||
211
skills/obsidian-canvas-creator/SKILL.md
Normal file
211
skills/obsidian-canvas-creator/SKILL.md
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
name: obsidian-canvas-creator
|
||||
description: Create Obsidian Canvas files from text content, supporting both MindMap and freeform layouts. Use this skill when users want to visualize content as an interactive canvas, create mind maps, or organize information spatially in Obsidian format.
|
||||
---
|
||||
|
||||
# Obsidian Canvas Creator
|
||||
|
||||
Transform text content into structured Obsidian Canvas files with support for MindMap and freeform layouts.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- User requests to create a canvas, mind map, or visual diagram from text
|
||||
- User wants to organize information spatially
|
||||
- User mentions "Obsidian Canvas" or similar visualization tools
|
||||
- Converting structured content (articles, notes, outlines) into visual format
|
||||
|
||||
## Core Workflow
|
||||
|
||||
### 1. Analyze Content
|
||||
|
||||
Read and understand the input content:
|
||||
- Identify main topics and hierarchical relationships
|
||||
- Extract key points, facts, and supporting details
|
||||
- Note any existing structure (headings, lists, sections)
|
||||
|
||||
### 2. Determine Layout Type
|
||||
|
||||
Ask user to choose or infer from context:
|
||||
|
||||
**MindMap Layout:**
|
||||
- Radial structure from center
|
||||
- Parent-child relationships
|
||||
- Clear hierarchy
|
||||
- Good for: brainstorming, topic exploration, hierarchical content
|
||||
|
||||
**Freeform Layout:**
|
||||
- Custom positioning
|
||||
- Flexible relationships
|
||||
- Multiple connection types
|
||||
- Good for: complex networks, non-hierarchical content, custom arrangements
|
||||
|
||||
### 3. Plan Structure
|
||||
|
||||
**For MindMap:**
|
||||
- Identify central concept (root node)
|
||||
- Map primary branches (main topics)
|
||||
- Organize secondary branches (subtopics)
|
||||
- Position leaf nodes (details)
|
||||
|
||||
**For Freeform:**
|
||||
- Group related concepts
|
||||
- Identify connection patterns
|
||||
- Plan spatial zones
|
||||
- Consider visual flow
|
||||
|
||||
### 4. Generate Canvas
|
||||
|
||||
Create JSON following the Canvas specification:
|
||||
|
||||
**Node Creation:**
|
||||
- Assign unique 8-12 character hex IDs
|
||||
- Set appropriate dimensions based on content length
|
||||
- Apply consistent color schemes
|
||||
- Ensure no coordinate overlaps
|
||||
|
||||
**Edge Creation:**
|
||||
- Connect parent-child relationships
|
||||
- Use appropriate arrow styles
|
||||
- Add labels for complex relationships
|
||||
- Choose line styles (straight for hierarchy, curved for cross-references)
|
||||
|
||||
**Grouping (Optional):**
|
||||
- Create visual containers for related nodes
|
||||
- Use subtle background colors
|
||||
- Add descriptive labels
|
||||
|
||||
### 5. Apply Layout Algorithm
|
||||
|
||||
**MindMap Layout Calculations:**
|
||||
|
||||
Refer to `references/layout-algorithms.md` for detailed algorithms. Key principles:
|
||||
|
||||
- Center root at (0, 0)
|
||||
- Distribute primary nodes radially
|
||||
- Space secondary nodes based on sibling count
|
||||
- Maintain minimum spacing: 320px horizontal, 200px vertical
|
||||
|
||||
**Freeform Layout Principles:**
|
||||
|
||||
- Start with logical groupings
|
||||
- Position groups with clear separation
|
||||
- Connect across groups with curved edges
|
||||
- Balance visual weight across canvas
|
||||
|
||||
### 6. Validate and Output
|
||||
|
||||
Before outputting:
|
||||
|
||||
**Validation Checklist:**
|
||||
- All nodes have unique IDs
|
||||
- No coordinate overlaps (check distance > node dimensions + spacing)
|
||||
- All edges reference valid node IDs
|
||||
- Groups (if any) have labels
|
||||
- Colors use consistent format (hex or preset numbers)
|
||||
- JSON is properly escaped (Chinese quotes: 『』 for double, 「」 for single)
|
||||
|
||||
**Output Format:**
|
||||
- Complete, valid JSON Canvas file
|
||||
- No additional explanation text
|
||||
- Directly importable into Obsidian
|
||||
|
||||
## Node Sizing Guidelines
|
||||
|
||||
**Text Length-Based Sizing:**
|
||||
- Short text (<30 chars): 220 × 100 px
|
||||
- Medium text (30-60 chars): 260 × 120 px
|
||||
- Long text (60-100 chars): 320 × 140 px
|
||||
- Very long text (>100 chars): 320 × 180 px
|
||||
|
||||
## Color Schemes
|
||||
|
||||
**Preset Colors (Recommended):**
|
||||
- `"1"` - Red (warnings, important)
|
||||
- `"2"` - Orange (action items)
|
||||
- `"3"` - Yellow (questions, notes)
|
||||
- `"4"` - Green (positive, completed)
|
||||
- `"5"` - Cyan (information, details)
|
||||
- `"6"` - Purple (concepts, abstract)
|
||||
|
||||
**Custom Hex Colors:**
|
||||
Use for brand consistency or specific themes. Always use uppercase format: `"#4A90E2"`
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Quote Handling:**
|
||||
- Chinese double quotes → 『』
|
||||
- Chinese single quotes → 「」
|
||||
- English double quotes → `\"`
|
||||
|
||||
2. **ID Generation:**
|
||||
- 8-12 character random hex strings
|
||||
- Must be unique across all nodes and edges
|
||||
|
||||
3. **Z-Index Order:**
|
||||
- Output groups first (bottom layer)
|
||||
- Then subgroups
|
||||
- Finally text/link nodes (top layer)
|
||||
|
||||
4. **Spacing Requirements:**
|
||||
- Minimum horizontal: 320px between node centers
|
||||
- Minimum vertical: 200px between node centers
|
||||
- Account for node dimensions when calculating
|
||||
|
||||
5. **JSON Structure:**
|
||||
- Top level contains only `nodes` and `edges` arrays
|
||||
- No extra wrapping objects
|
||||
- No comments in output
|
||||
|
||||
6. **No Emoji:**
|
||||
- Do not use any Emoji symbols in node text
|
||||
- Use color coding or text labels for visual distinction instead
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple MindMap Request
|
||||
User: "Create a mind map about solar system planets"
|
||||
|
||||
Process:
|
||||
1. Identify center: "Solar System"
|
||||
2. Primary branches: Inner Planets, Outer Planets, Dwarf Planets
|
||||
3. Secondary nodes: Individual planets with key facts
|
||||
4. Apply radial layout
|
||||
5. Generate JSON with proper spacing
|
||||
|
||||
### Freeform Content Request
|
||||
User: "Turn this article into a canvas" + [article text]
|
||||
|
||||
Process:
|
||||
1. Extract article structure (intro, body sections, conclusion)
|
||||
2. Identify key concepts and relationships
|
||||
3. Group related sections spatially
|
||||
4. Connect with labeled edges
|
||||
5. Apply freeform layout with clear zones
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- **Canvas Specification**: `references/canvas-spec.md` - Complete JSON Canvas format specification
|
||||
- **Layout Algorithms**: `references/layout-algorithms.md` - Detailed positioning algorithms for both layout types
|
||||
|
||||
Load these references when:
|
||||
- Need specification details for edge cases
|
||||
- Implementing complex layout calculations
|
||||
- Troubleshooting validation errors
|
||||
|
||||
## Tips for Quality Canvases
|
||||
|
||||
1. **Keep text concise**: Each node should be scannable (<2 lines preferred)
|
||||
2. **Use hierarchy**: Group by importance and relationship
|
||||
3. **Balance the canvas**: Distribute nodes to avoid clustering
|
||||
4. **Strategic colors**: Use colors to encode meaning, not just decoration
|
||||
5. **Meaningful connections**: Only add edges that clarify relationships
|
||||
6. **Test in Obsidian**: Verify the output opens correctly
|
||||
|
||||
## Common Pitfalls to Avoid
|
||||
|
||||
- Overlapping nodes (always check distances)
|
||||
- Inconsistent quote escaping (breaks JSON parsing)
|
||||
- Missing group labels (causes sidebar navigation issues)
|
||||
- Too much text in nodes (use file nodes for long content)
|
||||
- Duplicate IDs (each must be unique)
|
||||
- Unconnected nodes (unless intentional islands)
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "group01",
|
||||
"type": "group",
|
||||
"x": -50,
|
||||
"y": -50,
|
||||
"width": 600,
|
||||
"height": 400,
|
||||
"label": "Group 1 - Core Concepts",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "group02",
|
||||
"type": "group",
|
||||
"x": 650,
|
||||
"y": -50,
|
||||
"width": 600,
|
||||
"height": 400,
|
||||
"label": "Group 2 - Applications",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "node01",
|
||||
"type": "text",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Concept A",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "node02",
|
||||
"type": "text",
|
||||
"x": 290,
|
||||
"y": 0,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Concept B",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "node03",
|
||||
"type": "text",
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Concept C",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "node04",
|
||||
"type": "text",
|
||||
"x": 290,
|
||||
"y": 150,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Concept D",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "node05",
|
||||
"type": "text",
|
||||
"x": 700,
|
||||
"y": 0,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Application 1",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "node06",
|
||||
"type": "text",
|
||||
"x": 990,
|
||||
"y": 0,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Application 2",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "node07",
|
||||
"type": "text",
|
||||
"x": 700,
|
||||
"y": 150,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Application 3",
|
||||
"color": "5"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e1",
|
||||
"fromNode": "node01",
|
||||
"fromSide": "bottom",
|
||||
"toNode": "node03",
|
||||
"toSide": "top",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e2",
|
||||
"fromNode": "node02",
|
||||
"fromSide": "bottom",
|
||||
"toNode": "node04",
|
||||
"toSide": "top",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e3",
|
||||
"fromNode": "node01",
|
||||
"fromSide": "right",
|
||||
"toNode": "node05",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow",
|
||||
"label": "leads to",
|
||||
"color": "3"
|
||||
},
|
||||
{
|
||||
"id": "e4",
|
||||
"fromNode": "node02",
|
||||
"fromSide": "right",
|
||||
"toNode": "node06",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow",
|
||||
"label": "enables",
|
||||
"color": "3"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "root001",
|
||||
"type": "text",
|
||||
"x": -150,
|
||||
"y": -60,
|
||||
"width": 300,
|
||||
"height": 120,
|
||||
"text": "# Central Topic\n\nMain concept goes here",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "branch01",
|
||||
"type": "text",
|
||||
"x": 250,
|
||||
"y": -200,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Branch 1\n\nFirst main idea",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "branch02",
|
||||
"type": "text",
|
||||
"x": 250,
|
||||
"y": -50,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Branch 2\n\nSecond main idea",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "branch03",
|
||||
"type": "text",
|
||||
"x": 250,
|
||||
"y": 100,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Branch 3\n\nThird main idea",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "detail01",
|
||||
"type": "text",
|
||||
"x": 550,
|
||||
"y": -200,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"text": "Detail A",
|
||||
"color": "6"
|
||||
},
|
||||
{
|
||||
"id": "detail02",
|
||||
"type": "text",
|
||||
"x": 550,
|
||||
"y": -100,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"text": "Detail B",
|
||||
"color": "6"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e1",
|
||||
"fromNode": "root001",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch01",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e2",
|
||||
"fromNode": "root001",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch02",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e3",
|
||||
"fromNode": "root001",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch03",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e4",
|
||||
"fromNode": "branch01",
|
||||
"fromSide": "right",
|
||||
"toNode": "detail01",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e5",
|
||||
"fromNode": "branch01",
|
||||
"fromSide": "right",
|
||||
"toNode": "detail02",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
}
|
||||
]
|
||||
}
|
||||
403
skills/obsidian-canvas-creator/references/canvas-spec.md
Normal file
403
skills/obsidian-canvas-creator/references/canvas-spec.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# JSON Canvas Specification for Obsidian
|
||||
|
||||
Version 1.0 — 2024-03-11
|
||||
|
||||
## Overview
|
||||
|
||||
JSON Canvas is a format for representing infinite canvas documents. This specification defines the structure for creating canvas files compatible with Obsidian.
|
||||
|
||||
## Top Level Structure
|
||||
|
||||
The root JSON object contains two optional arrays:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [...],
|
||||
"edges": [...]
|
||||
}
|
||||
```
|
||||
|
||||
- `nodes` (optional, array): All canvas objects (text, files, links, groups)
|
||||
- `edges` (optional, array): All connections between nodes
|
||||
|
||||
## Node Types
|
||||
|
||||
### Common Attributes
|
||||
|
||||
All nodes share these required attributes:
|
||||
|
||||
- `id` (required, string): Unique identifier for the node
|
||||
- `type` (required, string): Node type (`text`, `file`, `link`, `group`)
|
||||
- `x` (required, integer): X position in pixels
|
||||
- `y` (required, integer): Y position in pixels
|
||||
- `width` (required, integer): Width in pixels
|
||||
- `height` (required, integer): Height in pixels
|
||||
- `color` (optional, string/number): Color (hex `"#FF0000"` or preset `"1"`)
|
||||
|
||||
### Text Nodes
|
||||
|
||||
Store plain text with Markdown formatting.
|
||||
|
||||
**Required Attributes:**
|
||||
- `text` (string): Content in Markdown syntax
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "abc123",
|
||||
"type": "text",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 250,
|
||||
"height": 100,
|
||||
"text": "# Main Topic\n\nKey point here",
|
||||
"color": "4"
|
||||
}
|
||||
```
|
||||
|
||||
### File Nodes
|
||||
|
||||
Reference other files or attachments (images, PDFs, etc.).
|
||||
|
||||
**Required Attributes:**
|
||||
- `file` (string): Path to file in the vault
|
||||
|
||||
**Optional Attributes:**
|
||||
- `subpath` (string): Link to specific heading/block (starts with `#`)
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "def456",
|
||||
"type": "file",
|
||||
"x": 300,
|
||||
"y": 0,
|
||||
"width": 400,
|
||||
"height": 300,
|
||||
"file": "Images/diagram.png"
|
||||
}
|
||||
```
|
||||
|
||||
**With Subpath:**
|
||||
```json
|
||||
{
|
||||
"id": "ghi789",
|
||||
"type": "file",
|
||||
"x": 0,
|
||||
"y": 200,
|
||||
"width": 250,
|
||||
"height": 100,
|
||||
"file": "Notes/Meeting Notes.md",
|
||||
"subpath": "#Action Items"
|
||||
}
|
||||
```
|
||||
|
||||
### Link Nodes
|
||||
|
||||
Reference external URLs.
|
||||
|
||||
**Required Attributes:**
|
||||
- `url` (string): Full URL including protocol
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "jkl012",
|
||||
"type": "link",
|
||||
"x": 0,
|
||||
"y": -200,
|
||||
"width": 250,
|
||||
"height": 100,
|
||||
"url": "https://obsidian.md",
|
||||
"color": "5"
|
||||
}
|
||||
```
|
||||
|
||||
### Group Nodes
|
||||
|
||||
Visual containers for organizing related nodes.
|
||||
|
||||
**Optional Attributes:**
|
||||
- `label` (string): Text label for the group (recommended)
|
||||
- `background` (string): Path to background image
|
||||
- `backgroundStyle` (string): Image rendering style
|
||||
- `cover`: Fill entire node
|
||||
- `ratio`: Maintain aspect ratio
|
||||
- `repeat`: Tile as pattern
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "group1",
|
||||
"type": "group",
|
||||
"x": -50,
|
||||
"y": -50,
|
||||
"width": 600,
|
||||
"height": 400,
|
||||
"label": "Main Concepts",
|
||||
"color": "4"
|
||||
}
|
||||
```
|
||||
|
||||
**With Background:**
|
||||
```json
|
||||
{
|
||||
"id": "group2",
|
||||
"type": "group",
|
||||
"x": 700,
|
||||
"y": 0,
|
||||
"width": 500,
|
||||
"height": 600,
|
||||
"label": "Reference Materials",
|
||||
"background": "Images/texture.png",
|
||||
"backgroundStyle": "repeat"
|
||||
}
|
||||
```
|
||||
|
||||
## Z-Index and Layering
|
||||
|
||||
Nodes are displayed in array order:
|
||||
- **First node**: Bottom layer (rendered below others)
|
||||
- **Last node**: Top layer (rendered above others)
|
||||
|
||||
**Best Practice Order:**
|
||||
1. Group nodes (backgrounds)
|
||||
2. Sub-groups
|
||||
3. Regular nodes (text, file, link)
|
||||
|
||||
This ensures groups appear behind content.
|
||||
|
||||
## Edges (Connections)
|
||||
|
||||
Edges connect nodes with lines.
|
||||
|
||||
**Required Attributes:**
|
||||
- `id` (required, string): Unique identifier
|
||||
- `fromNode` (required, string): Starting node ID
|
||||
- `toNode` (required, string): Ending node ID
|
||||
|
||||
**Optional Attributes:**
|
||||
- `fromSide` (string): Starting edge side
|
||||
- Values: `top`, `right`, `bottom`, `left`
|
||||
- `fromEnd` (string): Start endpoint shape
|
||||
- Values: `none` (default), `arrow`
|
||||
- `toSide` (string): Ending edge side
|
||||
- Values: `top`, `right`, `bottom`, `left`
|
||||
- `toEnd` (string): End endpoint shape
|
||||
- Values: `arrow` (default), `none`
|
||||
- `color` (string/number): Edge color
|
||||
- `label` (string): Text label on edge
|
||||
|
||||
**Example - Simple Connection:**
|
||||
```json
|
||||
{
|
||||
"id": "edge1",
|
||||
"fromNode": "abc123",
|
||||
"toNode": "def456"
|
||||
}
|
||||
```
|
||||
|
||||
**Example - Fully Specified:**
|
||||
```json
|
||||
{
|
||||
"id": "edge2",
|
||||
"fromNode": "def456",
|
||||
"fromSide": "bottom",
|
||||
"fromEnd": "none",
|
||||
"toNode": "ghi789",
|
||||
"toSide": "top",
|
||||
"toEnd": "arrow",
|
||||
"color": "3",
|
||||
"label": "leads to"
|
||||
}
|
||||
```
|
||||
|
||||
## Color System
|
||||
|
||||
### Preset Colors
|
||||
|
||||
Use string numbers `"1"` through `"6"`:
|
||||
|
||||
- `"1"` - Red
|
||||
- `"2"` - Orange
|
||||
- `"3"` - Yellow
|
||||
- `"4"` - Green
|
||||
- `"5"` - Cyan
|
||||
- `"6"` - Purple
|
||||
|
||||
**Note:** Exact colors adapt to Obsidian's theme. These provide semantic meaning across light/dark modes.
|
||||
|
||||
### Custom Hex Colors
|
||||
|
||||
Use hex format: `"#RRGGBB"`
|
||||
|
||||
**Examples:**
|
||||
- `"#4A90E2"` (blue)
|
||||
- `"#50E3C2"` (teal)
|
||||
- `"#F5A623"` (orange)
|
||||
|
||||
**Best Practice:** Use consistent format within a canvas (all hex OR all presets).
|
||||
|
||||
## Complete Example
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "group001",
|
||||
"type": "group",
|
||||
"x": -50,
|
||||
"y": -50,
|
||||
"width": 700,
|
||||
"height": 500,
|
||||
"label": "Core Concepts",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "center01",
|
||||
"type": "text",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 300,
|
||||
"height": 120,
|
||||
"text": "# Central Topic\n\nMain idea here",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "branch01",
|
||||
"type": "text",
|
||||
"x": 400,
|
||||
"y": -100,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Subtopic A",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "branch02",
|
||||
"type": "text",
|
||||
"x": 400,
|
||||
"y": 100,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Subtopic B",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "detail01",
|
||||
"type": "text",
|
||||
"x": 700,
|
||||
"y": -100,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"text": "Detail 1",
|
||||
"color": "6"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e1",
|
||||
"fromNode": "center01",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch01",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e2",
|
||||
"fromNode": "center01",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch02",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e3",
|
||||
"fromNode": "branch01",
|
||||
"fromSide": "right",
|
||||
"toNode": "detail01",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow",
|
||||
"color": "3"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Requirements
|
||||
|
||||
When creating canvas files, ensure:
|
||||
|
||||
1. **Unique IDs**: All `id` values must be unique across nodes and edges
|
||||
2. **Valid References**: All edge `fromNode` and `toNode` must reference existing node IDs
|
||||
3. **Required Fields**: All required attributes are present for each type
|
||||
4. **Valid Coordinates**: All position/dimension values are integers
|
||||
5. **Color Format**: Colors use either hex (`"#RRGGBB"`) or preset strings (`"1"` to `"6"`)
|
||||
6. **Quote Escaping**: Special characters properly escaped in JSON strings
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: Canvas won't open in Obsidian
|
||||
**Solutions:**
|
||||
- Validate JSON syntax (use JSON validator)
|
||||
- Check all IDs are unique
|
||||
- Verify all edge references exist
|
||||
- Ensure required fields present
|
||||
|
||||
### Issue: Nodes appear overlapped
|
||||
**Solutions:**
|
||||
- Increase spacing between coordinates
|
||||
- Account for node dimensions in positioning
|
||||
- Use minimum spacing: 320px horizontal, 200px vertical
|
||||
|
||||
### Issue: Groups don't show properly
|
||||
**Solutions:**
|
||||
- Ensure groups appear before content nodes in array
|
||||
- Add explicit `label` to all groups
|
||||
- Check group dimensions encompass child nodes
|
||||
|
||||
### Issue: Colors don't match expectations
|
||||
**Solutions:**
|
||||
- Use consistent color format (all hex OR all presets)
|
||||
- Remember presets adapt to theme
|
||||
- Test in both light and dark mode if using custom colors
|
||||
|
||||
### Issue: Text appears truncated
|
||||
**Solutions:**
|
||||
- Increase node dimensions
|
||||
- Break long text into multiple nodes
|
||||
- Use file nodes for lengthy content
|
||||
|
||||
## Character Encoding for Chinese Content
|
||||
|
||||
When canvas contains Chinese text, apply these transformations:
|
||||
|
||||
- Chinese double quotes `"` → `『』`
|
||||
- Chinese single quotes `'` → `「」`
|
||||
- English double quotes must be escaped: `\"`
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"text": "『核心概念』包含:「子概念A」和「子概念B」"
|
||||
}
|
||||
```
|
||||
|
||||
This prevents JSON parsing errors with mixed-language content.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Large Canvases**: Keep node count reasonable (<500 for smooth performance)
|
||||
- **Image Files**: Use compressed images for backgrounds
|
||||
- **Text Length**: Keep node text concise; use file nodes for long content
|
||||
- **Edge Complexity**: Minimize crossing edges for clarity
|
||||
|
||||
## Future Extensions
|
||||
|
||||
This specification may be extended with:
|
||||
- Additional node types
|
||||
- More edge styling options
|
||||
- Animation properties
|
||||
- Interactive behaviors
|
||||
|
||||
Always check Obsidian documentation for latest Canvas features.
|
||||
614
skills/obsidian-canvas-creator/references/layout-algorithms.md
Normal file
614
skills/obsidian-canvas-creator/references/layout-algorithms.md
Normal file
@@ -0,0 +1,614 @@
|
||||
# Layout Algorithms for Obsidian Canvas
|
||||
|
||||
Detailed algorithms for positioning nodes in MindMap and Freeform layouts.
|
||||
|
||||
## Layout Principles
|
||||
|
||||
### Universal Spacing Constants
|
||||
|
||||
```
|
||||
HORIZONTAL_SPACING = 320 // Minimum horizontal space between node centers
|
||||
VERTICAL_SPACING = 200 // Minimum vertical space between node centers
|
||||
NODE_PADDING = 20 // Internal padding within nodes
|
||||
```
|
||||
|
||||
### Collision Detection
|
||||
|
||||
Before finalizing any node position, verify:
|
||||
|
||||
```python
|
||||
def check_collision(node1, node2):
|
||||
"""Returns True if nodes overlap or are too close"""
|
||||
center1_x = node1.x + node1.width / 2
|
||||
center1_y = node1.y + node1.height / 2
|
||||
center2_x = node2.x + node2.width / 2
|
||||
center2_y = node2.y + node2.height / 2
|
||||
|
||||
dx = abs(center1_x - center2_x)
|
||||
dy = abs(center1_y - center2_y)
|
||||
|
||||
min_dx = (node1.width + node2.width) / 2 + HORIZONTAL_SPACING
|
||||
min_dy = (node1.height + node2.height) / 2 + VERTICAL_SPACING
|
||||
|
||||
return dx < min_dx or dy < min_dy
|
||||
```
|
||||
|
||||
## MindMap Layout Algorithm
|
||||
|
||||
### 1. Radial Tree Layout
|
||||
|
||||
Place root at center, arrange children radially.
|
||||
|
||||
#### Step 1: Position Root Node
|
||||
|
||||
```python
|
||||
root = {
|
||||
"x": 0 - (root_width / 2), # Center horizontally
|
||||
"y": 0 - (root_height / 2), # Center vertically
|
||||
"width": root_width,
|
||||
"height": root_height
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2: Calculate Primary Branch Positions
|
||||
|
||||
Distribute first-level children around root:
|
||||
|
||||
```python
|
||||
def position_primary_branches(root, children, radius=400):
|
||||
"""Position first-level children in a circle around root"""
|
||||
n = len(children)
|
||||
angle_step = 2 * pi / n
|
||||
|
||||
positions = []
|
||||
for i, child in enumerate(children):
|
||||
angle = i * angle_step
|
||||
|
||||
# Calculate position on circle
|
||||
x = root.center_x + radius * cos(angle) - child.width / 2
|
||||
y = root.center_y + radius * sin(angle) - child.height / 2
|
||||
|
||||
positions.append({"x": x, "y": y})
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
**Radius Selection:**
|
||||
- Small canvases (≤10 children): 400px
|
||||
- Medium canvases (11-20 children): 500px
|
||||
- Large canvases (>20 children): 600px
|
||||
|
||||
#### Step 3: Position Secondary Branches
|
||||
|
||||
For each primary branch, arrange its children:
|
||||
|
||||
**Horizontal Layout** (preferred for most cases):
|
||||
|
||||
```python
|
||||
def position_secondary_horizontal(parent, children, distance=350):
|
||||
"""Arrange children horizontally to the right of parent"""
|
||||
n = len(children)
|
||||
total_height = sum(child.height for child in children)
|
||||
total_spacing = (n - 1) * VERTICAL_SPACING
|
||||
|
||||
# Start position (top of vertical arrangement)
|
||||
start_y = parent.center_y - (total_height + total_spacing) / 2
|
||||
|
||||
positions = []
|
||||
current_y = start_y
|
||||
|
||||
for child in children:
|
||||
x = parent.x + parent.width + distance
|
||||
y = current_y
|
||||
|
||||
positions.append({"x": x, "y": y})
|
||||
current_y += child.height + VERTICAL_SPACING
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
**Vertical Layout** (for left/right primary branches):
|
||||
|
||||
```python
|
||||
def position_secondary_vertical(parent, children, distance=250):
|
||||
"""Arrange children vertically below parent"""
|
||||
n = len(children)
|
||||
total_width = sum(child.width for child in children)
|
||||
total_spacing = (n - 1) * HORIZONTAL_SPACING
|
||||
|
||||
# Start position (left of horizontal arrangement)
|
||||
start_x = parent.center_x - (total_width + total_spacing) / 2
|
||||
|
||||
positions = []
|
||||
current_x = start_x
|
||||
|
||||
for child in children:
|
||||
x = current_x
|
||||
y = parent.y + parent.height + distance
|
||||
|
||||
positions.append({"x": x, "y": y})
|
||||
current_x += child.width + HORIZONTAL_SPACING
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
#### Step 4: Balance and Adjust
|
||||
|
||||
After initial placement, check for collisions and adjust:
|
||||
|
||||
```python
|
||||
def balance_layout(nodes):
|
||||
"""Adjust nodes to prevent overlaps"""
|
||||
max_iterations = 10
|
||||
|
||||
for iteration in range(max_iterations):
|
||||
collisions = find_all_collisions(nodes)
|
||||
if not collisions:
|
||||
break
|
||||
|
||||
for node1, node2 in collisions:
|
||||
# Move node2 away from node1
|
||||
dx = node2.center_x - node1.center_x
|
||||
dy = node2.center_y - node1.center_y
|
||||
distance = sqrt(dx*dx + dy*dy)
|
||||
|
||||
# Calculate required distance
|
||||
min_dist = calculate_min_distance(node1, node2)
|
||||
|
||||
if distance > 0:
|
||||
# Move proportionally
|
||||
move_x = (dx / distance) * (min_dist - distance) / 2
|
||||
move_y = (dy / distance) * (min_dist - distance) / 2
|
||||
|
||||
node2.x += move_x
|
||||
node2.y += move_y
|
||||
```
|
||||
|
||||
### 2. Tree Layout (Hierarchical Top-Down)
|
||||
|
||||
Alternative for deep hierarchies.
|
||||
|
||||
#### Positioning Formula
|
||||
|
||||
```python
|
||||
def position_tree_layout(root, tree):
|
||||
"""Top-down tree layout"""
|
||||
# Level 0 (root)
|
||||
root.x = 0 - root.width / 2
|
||||
root.y = 0 - root.height / 2
|
||||
|
||||
# Process each level
|
||||
for level in range(1, max_depth):
|
||||
nodes_at_level = get_nodes_at_level(tree, level)
|
||||
|
||||
# Calculate horizontal spacing
|
||||
total_width = sum(node.width for node in nodes_at_level)
|
||||
total_spacing = (len(nodes_at_level) - 1) * HORIZONTAL_SPACING
|
||||
|
||||
start_x = -(total_width + total_spacing) / 2
|
||||
y = level * (150 + VERTICAL_SPACING) # 150px level height
|
||||
|
||||
current_x = start_x
|
||||
for node in nodes_at_level:
|
||||
node.x = current_x
|
||||
node.y = y
|
||||
current_x += node.width + HORIZONTAL_SPACING
|
||||
```
|
||||
|
||||
## Freeform Layout Algorithm
|
||||
|
||||
### 1. Content-Based Grouping
|
||||
|
||||
First, identify natural groupings in content:
|
||||
|
||||
```python
|
||||
def identify_groups(nodes, content_structure):
|
||||
"""Group nodes by semantic relationships"""
|
||||
groups = []
|
||||
|
||||
# Analyze content structure
|
||||
for section in content_structure:
|
||||
group_nodes = [node for node in nodes if node.section == section]
|
||||
|
||||
if len(group_nodes) > 1:
|
||||
groups.append({
|
||||
"label": section.title,
|
||||
"nodes": group_nodes
|
||||
})
|
||||
|
||||
return groups
|
||||
```
|
||||
|
||||
### 2. Grid-Based Zone Layout
|
||||
|
||||
Divide canvas into zones for different groups:
|
||||
|
||||
```python
|
||||
def layout_zones(groups, canvas_width=2000, canvas_height=1500):
|
||||
"""Arrange groups in grid zones"""
|
||||
n_groups = len(groups)
|
||||
|
||||
# Calculate grid dimensions
|
||||
cols = ceil(sqrt(n_groups))
|
||||
rows = ceil(n_groups / cols)
|
||||
|
||||
zone_width = canvas_width / cols
|
||||
zone_height = canvas_height / rows
|
||||
|
||||
# Assign zones
|
||||
zones = []
|
||||
for i, group in enumerate(groups):
|
||||
col = i % cols
|
||||
row = i // cols
|
||||
|
||||
zone = {
|
||||
"x": col * zone_width - canvas_width / 2,
|
||||
"y": row * zone_height - canvas_height / 2,
|
||||
"width": zone_width * 0.9, # Leave 10% margin
|
||||
"height": zone_height * 0.9,
|
||||
"group": group
|
||||
}
|
||||
zones.append(zone)
|
||||
|
||||
return zones
|
||||
```
|
||||
|
||||
### 3. Within-Zone Node Positioning
|
||||
|
||||
Position nodes within each zone:
|
||||
|
||||
**Option A: Organic Flow**
|
||||
|
||||
```python
|
||||
def position_organic(zone, nodes):
|
||||
"""Organic, flowing arrangement within zone"""
|
||||
positions = []
|
||||
|
||||
# Start at zone top-left with margin
|
||||
current_x = zone.x + 50
|
||||
current_y = zone.y + 50
|
||||
row_height = 0
|
||||
|
||||
for node in nodes:
|
||||
# Check if node fits in current row
|
||||
if current_x + node.width > zone.x + zone.width - 50:
|
||||
# Move to next row
|
||||
current_x = zone.x + 50
|
||||
current_y += row_height + VERTICAL_SPACING
|
||||
row_height = 0
|
||||
|
||||
positions.append({
|
||||
"x": current_x,
|
||||
"y": current_y
|
||||
})
|
||||
|
||||
current_x += node.width + HORIZONTAL_SPACING
|
||||
row_height = max(row_height, node.height)
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
**Option B: Structured Grid**
|
||||
|
||||
```python
|
||||
def position_grid(zone, nodes):
|
||||
"""Grid arrangement within zone"""
|
||||
n = len(nodes)
|
||||
cols = ceil(sqrt(n))
|
||||
rows = ceil(n / cols)
|
||||
|
||||
cell_width = (zone.width - 100) / cols # 50px margin each side
|
||||
cell_height = (zone.height - 100) / rows
|
||||
|
||||
positions = []
|
||||
for i, node in enumerate(nodes):
|
||||
col = i % cols
|
||||
row = i // cols
|
||||
|
||||
# Center node in cell
|
||||
x = zone.x + 50 + col * cell_width + (cell_width - node.width) / 2
|
||||
y = zone.y + 50 + row * cell_height + (cell_height - node.height) / 2
|
||||
|
||||
positions.append({"x": x, "y": y})
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
### 4. Cross-Zone Connections
|
||||
|
||||
Calculate optimal edge paths between zones:
|
||||
|
||||
```python
|
||||
def calculate_edge_path(from_node, to_node):
|
||||
"""Determine edge connection points"""
|
||||
# Calculate centers
|
||||
from_center = (from_node.x + from_node.width/2,
|
||||
from_node.y + from_node.height/2)
|
||||
to_center = (to_node.x + to_node.width/2,
|
||||
to_node.y + to_node.height/2)
|
||||
|
||||
# Determine best sides to connect
|
||||
dx = to_center[0] - from_center[0]
|
||||
dy = to_center[1] - from_center[1]
|
||||
|
||||
# Choose sides based on direction
|
||||
if abs(dx) > abs(dy):
|
||||
# Horizontal connection
|
||||
from_side = "right" if dx > 0 else "left"
|
||||
to_side = "left" if dx > 0 else "right"
|
||||
else:
|
||||
# Vertical connection
|
||||
from_side = "bottom" if dy > 0 else "top"
|
||||
to_side = "top" if dy > 0 else "bottom"
|
||||
|
||||
return {
|
||||
"fromSide": from_side,
|
||||
"toSide": to_side
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Force-Directed Layout
|
||||
|
||||
For complex networks with many cross-connections:
|
||||
|
||||
```python
|
||||
def force_directed_layout(nodes, edges, iterations=100):
|
||||
"""Spring-based layout algorithm"""
|
||||
# Constants
|
||||
SPRING_LENGTH = 200
|
||||
SPRING_CONSTANT = 0.1
|
||||
REPULSION_CONSTANT = 5000
|
||||
|
||||
for iteration in range(iterations):
|
||||
# Calculate repulsive forces (all pairs)
|
||||
for node1 in nodes:
|
||||
force_x, force_y = 0, 0
|
||||
|
||||
for node2 in nodes:
|
||||
if node1 == node2:
|
||||
continue
|
||||
|
||||
dx = node1.x - node2.x
|
||||
dy = node1.y - node2.y
|
||||
distance = sqrt(dx*dx + dy*dy)
|
||||
|
||||
if distance > 0:
|
||||
# Repulsive force
|
||||
force = REPULSION_CONSTANT / (distance * distance)
|
||||
force_x += (dx / distance) * force
|
||||
force_y += (dy / distance) * force
|
||||
|
||||
node1.force_x = force_x
|
||||
node1.force_y = force_y
|
||||
|
||||
# Calculate attractive forces (connected nodes)
|
||||
for edge in edges:
|
||||
node1 = get_node(edge.fromNode)
|
||||
node2 = get_node(edge.toNode)
|
||||
|
||||
dx = node2.x - node1.x
|
||||
dy = node2.y - node1.y
|
||||
distance = sqrt(dx*dx + dy*dy)
|
||||
|
||||
# Spring force
|
||||
force = SPRING_CONSTANT * (distance - SPRING_LENGTH)
|
||||
|
||||
node1.force_x += (dx / distance) * force
|
||||
node1.force_y += (dy / distance) * force
|
||||
node2.force_x -= (dx / distance) * force
|
||||
node2.force_y -= (dy / distance) * force
|
||||
|
||||
# Apply forces
|
||||
for node in nodes:
|
||||
node.x += node.force_x
|
||||
node.y += node.force_y
|
||||
```
|
||||
|
||||
### Hierarchical Clustering
|
||||
|
||||
Group related nodes automatically:
|
||||
|
||||
```python
|
||||
def hierarchical_cluster(nodes, similarity_threshold=0.7):
|
||||
"""Cluster nodes by content similarity"""
|
||||
clusters = []
|
||||
|
||||
# Calculate similarity matrix
|
||||
similarity = calculate_similarity_matrix(nodes)
|
||||
|
||||
# Agglomerative clustering
|
||||
current_clusters = [[node] for node in nodes]
|
||||
|
||||
while len(current_clusters) > 1:
|
||||
# Find most similar clusters
|
||||
max_sim = 0
|
||||
merge_i, merge_j = 0, 1
|
||||
|
||||
for i in range(len(current_clusters)):
|
||||
for j in range(i + 1, len(current_clusters)):
|
||||
sim = cluster_similarity(current_clusters[i],
|
||||
current_clusters[j],
|
||||
similarity)
|
||||
if sim > max_sim:
|
||||
max_sim = sim
|
||||
merge_i, merge_j = i, j
|
||||
|
||||
if max_sim < similarity_threshold:
|
||||
break
|
||||
|
||||
# Merge clusters
|
||||
current_clusters[merge_i].extend(current_clusters[merge_j])
|
||||
current_clusters.pop(merge_j)
|
||||
|
||||
return current_clusters
|
||||
```
|
||||
|
||||
## Layout Optimization
|
||||
|
||||
### Minimize Edge Crossings
|
||||
|
||||
```python
|
||||
def minimize_crossings(nodes, edges):
|
||||
"""Reduce edge crossing through node repositioning"""
|
||||
crossings = count_crossings(edges)
|
||||
|
||||
# Try swapping adjacent nodes
|
||||
improved = True
|
||||
while improved:
|
||||
improved = False
|
||||
|
||||
for i in range(len(nodes) - 1):
|
||||
# Swap nodes i and i+1
|
||||
swap_positions(nodes[i], nodes[i+1])
|
||||
new_crossings = count_crossings(edges)
|
||||
|
||||
if new_crossings < crossings:
|
||||
crossings = new_crossings
|
||||
improved = True
|
||||
else:
|
||||
# Swap back
|
||||
swap_positions(nodes[i], nodes[i+1])
|
||||
```
|
||||
|
||||
### Visual Balance
|
||||
|
||||
```python
|
||||
def calculate_visual_weight(canvas):
|
||||
"""Calculate center of mass for visual balance"""
|
||||
total_weight = 0
|
||||
weighted_x = 0
|
||||
weighted_y = 0
|
||||
|
||||
for node in canvas.nodes:
|
||||
# Weight is proportional to area
|
||||
weight = node.width * node.height
|
||||
total_weight += weight
|
||||
|
||||
weighted_x += node.center_x * weight
|
||||
weighted_y += node.center_y * weight
|
||||
|
||||
center_x = weighted_x / total_weight
|
||||
center_y = weighted_y / total_weight
|
||||
|
||||
# Shift entire canvas to center at (0, 0)
|
||||
offset_x = -center_x
|
||||
offset_y = -center_y
|
||||
|
||||
for node in canvas.nodes:
|
||||
node.x += offset_x
|
||||
node.y += offset_y
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Spatial Indexing
|
||||
|
||||
For large canvases, use spatial indexing to speed up collision detection:
|
||||
|
||||
```python
|
||||
class SpatialGrid:
|
||||
"""Grid-based spatial index for fast collision detection"""
|
||||
|
||||
def __init__(self, cell_size=500):
|
||||
self.cell_size = cell_size
|
||||
self.grid = {}
|
||||
|
||||
def add_node(self, node):
|
||||
"""Add node to grid"""
|
||||
cells = self.get_cells(node)
|
||||
for cell in cells:
|
||||
if cell not in self.grid:
|
||||
self.grid[cell] = []
|
||||
self.grid[cell].append(node)
|
||||
|
||||
def get_cells(self, node):
|
||||
"""Get grid cells node occupies"""
|
||||
min_x = int(node.x / self.cell_size)
|
||||
max_x = int((node.x + node.width) / self.cell_size)
|
||||
min_y = int(node.y / self.cell_size)
|
||||
max_y = int((node.y + node.height) / self.cell_size)
|
||||
|
||||
cells = []
|
||||
for x in range(min_x, max_x + 1):
|
||||
for y in range(min_y, max_y + 1):
|
||||
cells.append((x, y))
|
||||
return cells
|
||||
|
||||
def get_nearby_nodes(self, node):
|
||||
"""Get nodes in nearby cells"""
|
||||
cells = self.get_cells(node)
|
||||
nearby = set()
|
||||
|
||||
for cell in cells:
|
||||
if cell in self.grid:
|
||||
nearby.update(self.grid[cell])
|
||||
|
||||
return nearby
|
||||
```
|
||||
|
||||
## Common Layout Patterns
|
||||
|
||||
### Timeline Layout
|
||||
|
||||
For chronological content:
|
||||
|
||||
```python
|
||||
def layout_timeline(events, direction="horizontal"):
|
||||
"""Create timeline layout"""
|
||||
if direction == "horizontal":
|
||||
for i, event in enumerate(events):
|
||||
event.x = i * (event.width + HORIZONTAL_SPACING)
|
||||
event.y = 0
|
||||
else: # vertical
|
||||
for i, event in enumerate(events):
|
||||
event.x = 0
|
||||
event.y = i * (event.height + VERTICAL_SPACING)
|
||||
```
|
||||
|
||||
### Circular Layout
|
||||
|
||||
For cyclical processes:
|
||||
|
||||
```python
|
||||
def layout_circular(nodes, radius=500):
|
||||
"""Arrange nodes in a circle"""
|
||||
n = len(nodes)
|
||||
angle_step = 2 * pi / n
|
||||
|
||||
for i, node in enumerate(nodes):
|
||||
angle = i * angle_step
|
||||
node.x = radius * cos(angle) - node.width / 2
|
||||
node.y = radius * sin(angle) - node.height / 2
|
||||
```
|
||||
|
||||
### Matrix Layout
|
||||
|
||||
For comparing multiple dimensions:
|
||||
|
||||
```python
|
||||
def layout_matrix(nodes, rows, cols):
|
||||
"""Arrange nodes in a matrix"""
|
||||
cell_width = 400
|
||||
cell_height = 250
|
||||
|
||||
for i, node in enumerate(nodes):
|
||||
row = i // cols
|
||||
col = i % cols
|
||||
|
||||
node.x = col * cell_width
|
||||
node.y = row * cell_height
|
||||
```
|
||||
|
||||
## Quality Checks
|
||||
|
||||
Before finalizing layout, verify:
|
||||
|
||||
1. **No Overlaps**: All nodes have minimum spacing
|
||||
2. **Balanced**: Visual center near (0, 0)
|
||||
3. **Accessible**: All nodes reachable via edges
|
||||
4. **Readable**: Text sizes appropriate for zoom level
|
||||
5. **Efficient**: Edge paths reasonably direct
|
||||
|
||||
Use these algorithms as foundations, adapting to specific content and user preferences.
|
||||
Reference in New Issue
Block a user