@yanqirenshi/model.unchi 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018-2026 Satoshi Iwasaki (yanqirenshi@gmail.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,126 @@
1
- # Temporary Holding Version
1
+ # @yanqirenshi/model.unchi
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ d3.unchi(ポンチ絵)の**図ごとの知識**を、描画(view)から独立させたパッケージです。
4
+ react / d3 に依存しない、純粋なデータと関数だけを持ちます。
5
+
6
+ | 持つもの | 入口 |
7
+ |---|---|
8
+ | 入力データのスキーマ(JSON Schema 2020-12) | `SCHEMAS.nodes` / `SCHEMAS.edges` |
9
+ | コレクションをまたぐ整合性ルール(id の重複・参照先の存在) | `check(data)` |
10
+ | d3.unchi が既定で描いている値の補完 | `withDefaults(record)` / `nodeStyle` / `edgeStyle` / `kindOf` |
11
+ | 編集できる項目の一覧(インスペクタ用) | `fieldsOf(record)` |
12
+ | UI の操作を保存する patch に変える変換 | `movePatch(node, move)` / `applyPatch(record, values)` |
13
+
14
+ 利用側(iHub など)は、モデルが返す `{ collection, id, patch }` を保存するだけで、図の種類を知らずに書けます。
15
+
16
+ ## 位置づけ
17
+
18
+ ```
19
+ 利用側 ──▶ d3.unchi (view) ──▶ (いまは独立。将来 model.unchi を読む)
20
+ ──▶ colonoscope (ModelInspector)
21
+ ──▶ model.unchi
22
+ ──▶ 自前の永続化
23
+ ```
24
+
25
+ model.unchi・view・colonoscope の間に横の依存はありません。組み合わせるのは利用側だけです。
26
+ これは [Foolsgolds/Assholes#138](https://github.com/Foolsgolds/Assholes/issues/138) の「最初の一歩」で、
27
+ d3.unchi はまだ model.unchi を読んでいません。既定値は当面二重に書いてあり、
28
+ `tests/defaults.test.ts` が d3.unchi のソースと突き合わせてずれを検出します。
29
+
30
+ ## 使い方
31
+
32
+ 「レコード」は d3.unchi の `data()` に渡す入力そのもの(`nodes` / `edges` の 1 要素)です。
33
+ d3.unchi はコールバックの `core` に入力のオブジェクトをそのまま渡すので、`core` がレコードです。
34
+
35
+ ```js
36
+ import { Rectum } from '@yanqirenshi/d3.unchi';
37
+ import { ModelInspector } from '@yanqirenshi/colonoscope';
38
+ import {
39
+ check, fieldsOf, titleOf, withDefaults, movePatch, applyPatch, mergePatch, ModelError,
40
+ } from '@yanqirenshi/model.unchi';
41
+
42
+ // 読み込んだデータの検査(スキーマで検証したあとに)
43
+ const errors = check({ nodes, edges }); // [] なら問題なし
44
+
45
+ const rectum = new Rectum({
46
+ callbacks: {
47
+ node: {
48
+ // ドラッグの終わり → 位置の patch
49
+ dragend: (core, node, move) => save(movePatch(core, move)),
50
+ // → { collection: 'nodes', id: 11, patch: { position: { x: 120, y: 40 } } }
51
+ contextmenu: (core) => setTarget(core),
52
+ },
53
+ edge: { contextmenu: (core) => setTarget(core) },
54
+ },
55
+ });
56
+
57
+ // インスペクタ。項目はモデルが決め、利用側は hide / override で調整する
58
+ <ModelInspector
59
+ target={target && withDefaults(target)} // 既定値を重ねたレコード(書いていない色も初期値に出る)
60
+ fields={fieldsOf}
61
+ title={titleOf}
62
+ hide={['type']}
63
+ onApply={(values) => {
64
+ try {
65
+ const change = applyPatch(target, values); // 変えた項目だけが patch に入る
66
+ save(change);
67
+ setTarget(mergePatch(target, change.patch)); // 表示を先に更新
68
+ } catch (e) {
69
+ if (e instanceof ModelError) alert(e.message); // 利用者向けの日本語
70
+ }
71
+ }}
72
+ />
73
+ ```
74
+
75
+ ### `applyPatch` の検証
76
+
77
+ | 項目 | 規則 |
78
+ |---|---|
79
+ | 幅・高さ | 1 以上。整数に丸める |
80
+ | 色(背景・枠線・線) | ブラウザでは `CSS.supports('color', v)`。CSS の無い環境(Node)では `#hex` / `rgb()` などの関数表記 / 英字の色名を通す緩い判定 |
81
+ | 枠線の種類 | `solid` / `dashed` / `dotted` |
82
+ | 枠線・線の太さ | 0 より大きい数 |
83
+ | 接続位置(角度) | 数。0 以上 360 未満の整数に正規化 |
84
+ | マーカー | `none` / `arrow` / `triangle` / `triangle-open` / `circle` / `circle-open` / `diamond` / `diamond-open` |
85
+ | ノードの名称 | 空にできない(d3.unchi は名称の無いノードに代わりの文字を描く) |
86
+ | 線のラベル | 空にすると消す(`{ label: { text: null } }` — JSON Merge Patch) |
87
+
88
+ 図に描いている値(`withDefaults` の結果)から**変えた項目だけ**を patch に入れます。
89
+ 既定の値まで書き込まないためです。変更が無ければ `patch` は `{}` です。
90
+
91
+ ### 編集できる項目(`fieldsOf`)
92
+
93
+ colonoscope の `{ path, label, type }` と同じ形で、取りうる値の一覧 `options` を足しています。
94
+ `path` はレコードの**実際の項目**を指します(`_style.*` のような写し先は作りません)。
95
+
96
+ - ノード: 名称(`label.text`)・種類・ID・幅・高さ・背景色・枠線の種類 / 色 / 太さ
97
+ - 線: ID・接続元 / 接続先のノードと位置(角度)・始点 / 終点のマーカー・線の色 / 太さ・ラベル
98
+ - 文字だけのノード(`type: 'TEXT'`): 枠・背景・名称の項目を除く(本文の編集は当面スコープ外)
99
+
100
+ ### スキーマ
101
+
102
+ ```js
103
+ import Ajv2020 from 'ajv/dist/2020.js';
104
+ import { SCHEMAS } from '@yanqirenshi/model.unchi';
105
+
106
+ const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true });
107
+ ajv.addKeyword({ keyword: 'x-id-start', schemaType: 'number' }); // 採番の注釈。検証には使わない
108
+ const validate = ajv.compile(SCHEMAS.nodes);
109
+ ```
110
+
111
+ スキーマが検査するのは図の構造にかかわる項目(id・type・_class・size・position・children、線の from / to・マーカー)だけで、
112
+ 見た目の項目は d3.unchi の入力の形のまま書けるよう `additionalProperties` を閉じていません。
113
+
114
+ ## スコープ外(いまは持たない)
115
+
116
+ - `TEXT` ノードの本文(`text` / `bullet`)、`ICON` / `CYLINDER` / `IMAGE` 固有の項目(`icon` / `cap` / `url`)の編集
117
+ - `padding` / `label.align` などレイアウト系の編集
118
+ - ファイル・HTTP などの永続化(利用側の実装)
119
+
120
+ ## コマンド
121
+
122
+ ```bash
123
+ npm run build -w @yanqirenshi/model.unchi # tsc で dist/ へビルド(d.ts 含む)
124
+ npm run typecheck -w @yanqirenshi/model.unchi
125
+ npm test -w @yanqirenshi/model.unchi # vitest(d3.unchi の既定値との突き合わせを含む)
126
+ ```
@@ -0,0 +1,13 @@
1
+ import type { UnchiGraph, UnchiNodeRecord } from './types.js';
2
+ /** 入れ子のノード(children)を平らにする(親 → 子 の順) */
3
+ export declare function flatten(nodes: UnchiNodeRecord[]): UnchiNodeRecord[];
4
+ /**
5
+ * 図全体を検査する。
6
+ * - nodes(子を含む)の id が重複していない
7
+ * - edges の id が重複していない
8
+ * - edges の from.id / to.id がノードに存在する
9
+ *
10
+ * @param prefix メッセージのコレクション名の前に付ける文字列(例: 'overview/')
11
+ */
12
+ export declare function check(data: UnchiGraph, prefix?: string): string[];
13
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9D,wCAAwC;AACxC,wBAAgB,OAAO,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAEnE;AAgBD;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,SAAK,GAAG,MAAM,EAAE,CAa7D"}
package/dist/check.js ADDED
@@ -0,0 +1,37 @@
1
+ // コレクションをまたぐ整合性の検査(id の重複・参照先の存在)。
2
+ // スキーマ(schema.ts)に合っているデータを渡す。戻り値はエラーのメッセージの配列(空なら問題なし)。
3
+ //
4
+ // iHub の server/validation/rules.mjs の checkGraph を移したもの。
5
+ // メッセージの先頭はコレクション名で、prefix を渡すと 'overview/nodes: ...' のように付けられる。
6
+ /** 入れ子のノード(children)を平らにする(親 → 子 の順) */
7
+ export function flatten(nodes) {
8
+ return nodes.flatMap(node => [node, ...flatten(node.children || [])]);
9
+ }
10
+ // list の中で id が重複していればエラーにする。id の集合を返す。
11
+ function checkUnique(errors, label, list) {
12
+ const values = new Set();
13
+ for (const d of list) {
14
+ if (values.has(d.id))
15
+ errors.push(`${label}: id が重複しています。(id=${String(d.id)})`);
16
+ values.add(d.id);
17
+ }
18
+ return values;
19
+ }
20
+ /**
21
+ * 図全体を検査する。
22
+ * - nodes(子を含む)の id が重複していない
23
+ * - edges の id が重複していない
24
+ * - edges の from.id / to.id がノードに存在する
25
+ *
26
+ * @param prefix メッセージのコレクション名の前に付ける文字列(例: 'overview/')
27
+ */
28
+ export function check(data, prefix = '') {
29
+ const errors = [];
30
+ const node_ids = checkUnique(errors, `${prefix}nodes`, flatten(data.nodes || []));
31
+ checkUnique(errors, `${prefix}edges`, data.edges || []);
32
+ for (const edge of data.edges || [])
33
+ for (const end of ['from', 'to'])
34
+ if (!edge[end] || !node_ids.has(edge[end].id))
35
+ errors.push(`${prefix}edges: ${end}.id のノードが見つかりません。(id=${String(edge.id)}, ${end}.id=${String(edge[end]?.id)})`);
36
+ return errors;
37
+ }
@@ -0,0 +1,77 @@
1
+ import type { Background, Border, Font, Stroke } from '@yanqirenshi/types';
2
+ import type { UnchiEdgeRecord, UnchiNodeClass, UnchiNodeRecord, UnchiRecord } from './types.js';
3
+ /** ノードの区分 */
4
+ export declare const NODE_CLASSES: readonly ["BRANCH", "REAF"];
5
+ /** 枠線の種類(d3.unchi が描けるもの) */
6
+ export declare const BORDER_TYPES: readonly ["solid", "dashed", "dotted"];
7
+ /** 線の両端のマーカー */
8
+ export declare const MARKER_TYPES: readonly ["none", "arrow", "triangle", "triangle-open", "circle", "circle-open", "diamond", "diamond-open"];
9
+ /**
10
+ * ノードの「描き方の種類」。d3.unchi がどのデータクラスで正規化するかに対応する。
11
+ * BRANCH / REAF … 通常の枝・葉(type: NODE / COMPONENT、_class か children で決まる)
12
+ * BRANCH_ICON … type: ICON(アイコン付きの枝)
13
+ * BRANCH_CYLINDER … type: CYLINDER(円筒の枝)
14
+ * REAF_IMAGE … type: IMAGE(画像の葉)
15
+ * REAF_CHEVRON … type: CHEVRON(矢羽の葉)
16
+ * REAF_TEXT … type: TEXT(文字だけの葉。枠・背景を描かない)
17
+ */
18
+ export type UnchiNodeKind = 'BRANCH' | 'BRANCH_ICON' | 'BRANCH_CYLINDER' | 'REAF' | 'REAF_IMAGE' | 'REAF_CHEVRON' | 'REAF_TEXT';
19
+ export interface UnchiNodeStyle {
20
+ border: Required<Border>;
21
+ background: Required<Background>;
22
+ }
23
+ /** 種類ごとの枠線・背景の既定(d3.unchi の各データクラスの styleDefaults()) */
24
+ export declare const NODE_STYLE_DEFAULTS: Record<UnchiNodeKind, UnchiNodeStyle>;
25
+ /** 通常ノードのラベルの既定(Node.dataTemplate の label) */
26
+ export declare const NODE_LABEL_DEFAULTS: {
27
+ readonly position: {
28
+ readonly x: 20;
29
+ readonly y: 20;
30
+ };
31
+ readonly font: Font;
32
+ readonly align: "left";
33
+ readonly valign: "top";
34
+ };
35
+ /** 線の既定(Edge.dataTemplate / labelFontDefaults) */
36
+ export declare const EDGE_DEFAULTS: {
37
+ readonly stroke: Required<Pick<Stroke, "color" | "width">>;
38
+ readonly from_marker: "none";
39
+ readonly to_marker: "arrow";
40
+ readonly label_font: Required<Font>;
41
+ };
42
+ /** 線か(from と to を持つ)。ノードは持たない */
43
+ export declare function isEdge(record: UnchiRecord | null | undefined): record is UnchiEdgeRecord;
44
+ /**
45
+ * ノードの区分。入力が BRANCH / REAF ならそのまま、
46
+ * それ以外(省略・不正値)は children があれば BRANCH、無ければ REAF。
47
+ * (d3.unchi の Node.classOf と同じ)
48
+ */
49
+ export declare function classOf(node: UnchiNodeRecord): UnchiNodeClass;
50
+ /**
51
+ * ノードの描き方の種類。d3.unchi の Rectum.nodeModel(core) と同じ順で決める。
52
+ * ICON / CYLINDER は、明示的に _class: 'REAF' でなければ枝
53
+ * それ以外は区分が BRANCH なら枝、IMAGE / CHEVRON / TEXT はそれぞれの葉、残りは通常の葉
54
+ */
55
+ export declare function kindOf(node: UnchiNodeRecord): UnchiNodeKind;
56
+ /** ノードが描かれる枠線・背景(既定に入力を重ねた、実際に描かれる値) */
57
+ export declare function nodeStyle(node: UnchiNodeRecord): UnchiNodeStyle;
58
+ /**
59
+ * 線が描かれる値。
60
+ * stroke は d3.unchi の Edge.normalize と同じく truthy 判定(空文字・0 は既定に倒れる)。
61
+ * マーカーは MARKER_TYPES に無ければ既定。
62
+ */
63
+ export declare function edgeStyle(edge: UnchiEdgeRecord): {
64
+ stroke: Required<Pick<Stroke, 'color' | 'width'>>;
65
+ from_marker: string;
66
+ to_marker: string;
67
+ };
68
+ /**
69
+ * レコードに、d3.unchi が既定で描いている値を重ねて返す(入力は書き換えない)。
70
+ * インスペクタの初期値に使う。children は重ねない(子は右クリックしたときに自分で通す)。
71
+ *
72
+ * 重ねる項目: ノードは label.text(無ければ '')・border・background、
73
+ * 線は stroke・from_marker・to_marker・label.text(無ければ '')。
74
+ * size / position / 角度は view が計算で埋めるもの(Hierarchy.fitting 等)なので、ここでは触らない。
75
+ */
76
+ export declare function withDefaults<T extends UnchiRecord>(record: T): T;
77
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEhG,aAAa;AACb,eAAO,MAAM,YAAY,6BAA8B,CAAC;AAExD,6BAA6B;AAC7B,eAAO,MAAM,YAAY,wCAAyC,CAAC;AAEnE,gBAAgB;AAChB,eAAO,MAAM,YAAY,6GAEf,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB,QAAQ,GAAG,aAAa,GAAG,iBAAiB,GAC5C,MAAM,GAAG,YAAY,GAAG,cAAc,GAAG,WAAW,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;CAClC;AAED,wDAAwD;AACxD,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,aAAa,EAAE,cAAc,CA8BrE,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,mBAAmB;;;;;mBAEU,IAAI;;;CAGpC,CAAC;AAEX,kDAAkD;AAClD,eAAO,MAAM,aAAa;qBACiB,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC;;;yBAGlB,QAAQ,CAAC,IAAI,CAAC;CACtE,CAAC;AAEX,iCAAiC;AACjC,wBAAgB,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,IAAI,eAAe,CAExF;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,eAAe,GAAG,cAAc,CAO7D;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,CAuB3D;AAgBD,wCAAwC;AACxC,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,cAAc,CAO/D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG;IAChD,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB,CAYA;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAsBhE"}
@@ -0,0 +1,159 @@
1
+ // d3.unchi が「書いていない項目」に既定で描く値。
2
+ //
3
+ // 値の出どころは d3.unchi の src/js/datamodels/*.js の styleDefaults() / dataTemplate() と、
4
+ // Rectum.nodeModel(core) の振り分け規則。**d3.unchi 側はまだこのファイルを読んでいない**ので
5
+ // 当面は二重に書いてあり、tests/defaults.test.ts が d3.unchi のソースと突き合わせて
6
+ // ずれを検出する(d3.unchi が model.unchi を読むようになったら、この二重は解消する — #138 の 3)。
7
+ /** ノードの区分 */
8
+ export const NODE_CLASSES = ['BRANCH', 'REAF'];
9
+ /** 枠線の種類(d3.unchi が描けるもの) */
10
+ export const BORDER_TYPES = ['solid', 'dashed', 'dotted'];
11
+ /** 線の両端のマーカー */
12
+ export const MARKER_TYPES = [
13
+ 'none', 'arrow', 'triangle', 'triangle-open', 'circle', 'circle-open', 'diamond', 'diamond-open',
14
+ ];
15
+ /** 種類ごとの枠線・背景の既定(d3.unchi の各データクラスの styleDefaults()) */
16
+ export const NODE_STYLE_DEFAULTS = {
17
+ BRANCH: {
18
+ border: { color: '#7a8ca3', width: 1.5, type: 'dashed', r: 10 },
19
+ background: { color: '#f3f6fb', opacity: 1.0 },
20
+ },
21
+ BRANCH_ICON: {
22
+ border: { color: '#545b64', width: 1.5, type: 'solid', r: 0 },
23
+ background: { color: '#ffffff', opacity: 1.0 },
24
+ },
25
+ BRANCH_CYLINDER: {
26
+ border: { color: '#666666', width: 2, type: 'solid', r: 0 },
27
+ background: { color: '#ffffff', opacity: 1.0 },
28
+ },
29
+ REAF: {
30
+ border: { color: '#5b7bb5', width: 1.5, type: 'solid', r: 4 },
31
+ background: { color: '#ffffff', opacity: 1.0 },
32
+ },
33
+ // ReafImage は styleDefaults を上書きしないので Reaf と同じ
34
+ REAF_IMAGE: {
35
+ border: { color: '#5b7bb5', width: 1.5, type: 'solid', r: 4 },
36
+ background: { color: '#ffffff', opacity: 1.0 },
37
+ },
38
+ REAF_CHEVRON: {
39
+ border: { color: '#5b7bb5', width: 1.5, type: 'solid', r: 0 },
40
+ background: { color: '#ffffff', opacity: 1.0 },
41
+ },
42
+ REAF_TEXT: {
43
+ border: { color: 'none', width: 0, type: 'solid', r: 0 },
44
+ background: { color: 'none', opacity: 0 },
45
+ },
46
+ };
47
+ /** 通常ノードのラベルの既定(Node.dataTemplate の label) */
48
+ export const NODE_LABEL_DEFAULTS = {
49
+ position: { x: 20, y: 20 },
50
+ font: { size: 16, color: '#333333' },
51
+ align: 'left',
52
+ valign: 'top',
53
+ };
54
+ /** 線の既定(Edge.dataTemplate / labelFontDefaults) */
55
+ export const EDGE_DEFAULTS = {
56
+ stroke: { color: '#333', width: 1.5 },
57
+ from_marker: 'none',
58
+ to_marker: 'arrow',
59
+ label_font: { size: 12, color: '#333333', weight: 'normal' },
60
+ };
61
+ /** 線か(from と to を持つ)。ノードは持たない */
62
+ export function isEdge(record) {
63
+ return !!(record && record.from && record.to);
64
+ }
65
+ /**
66
+ * ノードの区分。入力が BRANCH / REAF ならそのまま、
67
+ * それ以外(省略・不正値)は children があれば BRANCH、無ければ REAF。
68
+ * (d3.unchi の Node.classOf と同じ)
69
+ */
70
+ export function classOf(node) {
71
+ const v = node._class;
72
+ if (typeof v === 'string' && NODE_CLASSES.includes(v))
73
+ return v;
74
+ return node.children && node.children.length > 0 ? 'BRANCH' : 'REAF';
75
+ }
76
+ /**
77
+ * ノードの描き方の種類。d3.unchi の Rectum.nodeModel(core) と同じ順で決める。
78
+ * ICON / CYLINDER は、明示的に _class: 'REAF' でなければ枝
79
+ * それ以外は区分が BRANCH なら枝、IMAGE / CHEVRON / TEXT はそれぞれの葉、残りは通常の葉
80
+ */
81
+ export function kindOf(node) {
82
+ const type = node.type;
83
+ const explicit = NODE_CLASSES.includes(node._class) ? node._class : null;
84
+ if (type === 'ICON' && explicit !== 'REAF')
85
+ return 'BRANCH_ICON';
86
+ if (type === 'CYLINDER' && explicit !== 'REAF')
87
+ return 'BRANCH_CYLINDER';
88
+ if (classOf(node) === 'BRANCH')
89
+ return 'BRANCH';
90
+ if (type === 'IMAGE')
91
+ return 'REAF_IMAGE';
92
+ if (type === 'CHEVRON')
93
+ return 'REAF_CHEVRON';
94
+ if (type === 'TEXT')
95
+ return 'REAF_TEXT';
96
+ return 'REAF';
97
+ }
98
+ /** 既定に入力の指定(undefined / null でない項目だけ)を重ねる(d3.unchi の datamodels/style.js の mergeStyle) */
99
+ function mergeStyle(defaults, given) {
100
+ const out = { ...defaults };
101
+ for (const key of Object.keys(given || {})) {
102
+ const v = given[key];
103
+ if (v !== undefined && v !== null)
104
+ out[key] = v;
105
+ }
106
+ return out;
107
+ }
108
+ /** ノードが描かれる枠線・背景(既定に入力を重ねた、実際に描かれる値) */
109
+ export function nodeStyle(node) {
110
+ const defaults = NODE_STYLE_DEFAULTS[kindOf(node)];
111
+ return {
112
+ border: mergeStyle(defaults.border, node.border),
113
+ background: mergeStyle(defaults.background, node.background),
114
+ };
115
+ }
116
+ /**
117
+ * 線が描かれる値。
118
+ * stroke は d3.unchi の Edge.normalize と同じく truthy 判定(空文字・0 は既定に倒れる)。
119
+ * マーカーは MARKER_TYPES に無ければ既定。
120
+ */
121
+ export function edgeStyle(edge) {
122
+ const marker = (v, fallback) => MARKER_TYPES.includes(v) ? v : fallback;
123
+ return {
124
+ stroke: {
125
+ color: edge.stroke?.color || EDGE_DEFAULTS.stroke.color,
126
+ width: edge.stroke?.width || EDGE_DEFAULTS.stroke.width,
127
+ },
128
+ from_marker: marker(edge.from_marker, EDGE_DEFAULTS.from_marker),
129
+ to_marker: marker(edge.to_marker, EDGE_DEFAULTS.to_marker),
130
+ };
131
+ }
132
+ /**
133
+ * レコードに、d3.unchi が既定で描いている値を重ねて返す(入力は書き換えない)。
134
+ * インスペクタの初期値に使う。children は重ねない(子は右クリックしたときに自分で通す)。
135
+ *
136
+ * 重ねる項目: ノードは label.text(無ければ '')・border・background、
137
+ * 線は stroke・from_marker・to_marker・label.text(無ければ '')。
138
+ * size / position / 角度は view が計算で埋めるもの(Hierarchy.fitting 等)なので、ここでは触らない。
139
+ */
140
+ export function withDefaults(record) {
141
+ if (isEdge(record)) {
142
+ const style = edgeStyle(record);
143
+ const label = record.label && typeof record.label === 'object' ? record.label : {};
144
+ return {
145
+ ...record,
146
+ ...style,
147
+ label: { ...label, text: typeof label.text === 'string' ? label.text : '' },
148
+ };
149
+ }
150
+ const node = record;
151
+ const style = nodeStyle(node);
152
+ const label = node.label && typeof node.label === 'object' ? node.label : {};
153
+ return {
154
+ ...node,
155
+ label: { ...label, text: typeof label.text === 'string' ? label.text : '' },
156
+ border: style.border,
157
+ background: style.background,
158
+ };
159
+ }
@@ -0,0 +1,8 @@
1
+ import type { UnchiField, UnchiRecord } from './types.js';
2
+ /** ノードの項目 */
3
+ export declare const NODE_FIELDS: readonly UnchiField[];
4
+ /** 線の項目。接続位置はノードの中心から見た角度(0=真下・時計回り。90=左 / 180=真上 / 270=右) */
5
+ export declare const EDGE_FIELDS: readonly UnchiField[];
6
+ /** レコードに応じた項目の一覧(線 / ノード / 文字だけのノード) */
7
+ export declare function fieldsOf(record: UnchiRecord): UnchiField[];
8
+ //# sourceMappingURL=fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../src/fields.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE1D,aAAa;AACb,eAAO,MAAM,WAAW,EAAE,SAAS,UAAU,EAU5C,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,WAAW,EAAE,SAAS,UAAU,EAW5C,CAAC;AAMF,wCAAwC;AACxC,wBAAgB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,EAAE,CAQ1D"}
package/dist/fields.js ADDED
@@ -0,0 +1,42 @@
1
+ // 編集できる項目の一覧。インスペクタ(colonoscope の ModelInspector)に渡す。
2
+ //
3
+ // path はレコードの実際の項目を指す。既定値は withDefaults() で重ねてから渡すので、
4
+ // 書いていない項目(枠線の色など)も、図に描かれている値が初期値として出る。
5
+ // type: 'readonly' は表示だけ。options は取りうる値の一覧(検証にも使う)。
6
+ import { BORDER_TYPES, MARKER_TYPES, isEdge } from './defaults.js';
7
+ /** ノードの項目 */
8
+ export const NODE_FIELDS = [
9
+ { path: 'label.text', label: '名称', type: 'text' },
10
+ { path: 'type', label: '種類', type: 'readonly' },
11
+ { path: 'id', label: 'ID', type: 'readonly' },
12
+ { path: 'size.w', label: '幅', type: 'number' },
13
+ { path: 'size.h', label: '高さ', type: 'number' },
14
+ { path: 'background.color', label: '背景色', type: 'text' },
15
+ { path: 'border.type', label: '枠線の種類', type: 'text', options: BORDER_TYPES },
16
+ { path: 'border.color', label: '枠線の色', type: 'text' },
17
+ { path: 'border.width', label: '枠線の太さ', type: 'number' },
18
+ ];
19
+ /** 線の項目。接続位置はノードの中心から見た角度(0=真下・時計回り。90=左 / 180=真上 / 270=右) */
20
+ export const EDGE_FIELDS = [
21
+ { path: 'id', label: 'ID', type: 'readonly' },
22
+ { path: 'from.id', label: '接続元のノード', type: 'readonly' },
23
+ { path: 'from.position', label: '接続元の位置 (角度)', type: 'number' },
24
+ { path: 'to.id', label: '接続先のノード', type: 'readonly' },
25
+ { path: 'to.position', label: '接続先の位置 (角度)', type: 'number' },
26
+ { path: 'from_marker', label: '始点のマーカー', type: 'text', options: MARKER_TYPES },
27
+ { path: 'to_marker', label: '終点のマーカー', type: 'text', options: MARKER_TYPES },
28
+ { path: 'stroke.color', label: '線の色', type: 'text' },
29
+ { path: 'stroke.width', label: '線の太さ', type: 'number' },
30
+ { path: 'label.text', label: 'ラベル', type: 'text' },
31
+ ];
32
+ // 文字だけのノード(type: TEXT)は枠・背景を描かず、本文も label ではなく text にある。
33
+ // 本文の編集(配列・bullet)は当面スコープ外(#139)なので、該当する項目を外すだけにする。
34
+ const TEXT_NODE_HIDDEN = new Set(['label.text', 'background.color', 'border.type', 'border.color', 'border.width']);
35
+ /** レコードに応じた項目の一覧(線 / ノード / 文字だけのノード) */
36
+ export function fieldsOf(record) {
37
+ if (isEdge(record))
38
+ return [...EDGE_FIELDS];
39
+ if (record.type === 'TEXT')
40
+ return NODE_FIELDS.filter(f => !TEXT_NODE_HIDDEN.has(f.path));
41
+ return [...NODE_FIELDS];
42
+ }
@@ -0,0 +1,8 @@
1
+ export type { UnchiId, UnchiNodeClass, UnchiNodeLabel, UnchiNodeRecord, UnchiEdgeEnd, UnchiEdgeRecord, UnchiRecord, UnchiGraph, UnchiCollection, MergePatch, UnchiChange, UnchiMove, UnchiFieldType, UnchiField, UnchiValues, } from './types.js';
2
+ export type { UnchiNodeKind, UnchiNodeStyle } from './defaults.js';
3
+ export { NODE_CLASSES, BORDER_TYPES, MARKER_TYPES, NODE_STYLE_DEFAULTS, NODE_LABEL_DEFAULTS, EDGE_DEFAULTS, isEdge, classOf, kindOf, nodeStyle, edgeStyle, withDefaults, } from './defaults.js';
4
+ export { NODES_SCHEMA, EDGES_SCHEMA, SCHEMAS } from './schema.js';
5
+ export { check, flatten } from './check.js';
6
+ export { NODE_FIELDS, EDGE_FIELDS, fieldsOf } from './fields.js';
7
+ export { ModelError, collectionOf, titleOf, getByPath, mergePatch, movePatch, applyPatch, isColor, } from './patch.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,YAAY,EACV,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EACpG,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,GACzG,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,EACL,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,aAAa,EACjG,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,GAC5D,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EACL,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,GACzF,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // model.unchi — d3.unchi(ポンチ絵)の「図ごとの知識」。描画はしない(react / d3 に依存しない)。
2
+ //
3
+ // スキーマ: SCHEMAS / NODES_SCHEMA / EDGES_SCHEMA(JSON Schema 2020-12)
4
+ // 整合性ルール: check(data)(id の重複・参照先の存在)
5
+ // 既定値の補完: withDefaults(record) / nodeStyle / edgeStyle / kindOf / classOf
6
+ // 編集できる項目: fieldsOf(record) / NODE_FIELDS / EDGE_FIELDS
7
+ // patch への変換: movePatch(node, move) / applyPatch(record, values) / mergePatch(record, patch)
8
+ //
9
+ // view(d3.unchi)・インスペクタ(colonoscope)との間に横の依存は無い。組み合わせるのは利用側。
10
+ export { NODE_CLASSES, BORDER_TYPES, MARKER_TYPES, NODE_STYLE_DEFAULTS, NODE_LABEL_DEFAULTS, EDGE_DEFAULTS, isEdge, classOf, kindOf, nodeStyle, edgeStyle, withDefaults, } from './defaults.js';
11
+ export { NODES_SCHEMA, EDGES_SCHEMA, SCHEMAS } from './schema.js';
12
+ export { check, flatten } from './check.js';
13
+ export { NODE_FIELDS, EDGE_FIELDS, fieldsOf } from './fields.js';
14
+ export { ModelError, collectionOf, titleOf, getByPath, mergePatch, movePatch, applyPatch, isColor, } from './patch.js';
@@ -0,0 +1,36 @@
1
+ import type { MergePatch, UnchiChange, UnchiCollection, UnchiMove, UnchiNodeRecord, UnchiRecord, UnchiValues } from './types.js';
2
+ /** 入力の検証エラー。message は利用者向け */
3
+ export declare class ModelError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /** レコードを置くコレクション */
7
+ export declare function collectionOf(record: UnchiRecord): UnchiCollection;
8
+ /** インスペクタの見出し。ラベルの文字、無ければ「種類 id」 */
9
+ export declare function titleOf(record: UnchiRecord): string;
10
+ /** ドット区切りの path で値を取り出す */
11
+ export declare function getByPath(obj: unknown, path: string): unknown;
12
+ /**
13
+ * JSON Merge Patch をレコードに重ねたものを返す(入力は書き換えない)。
14
+ * null の項目は除く。保存したあと、表示を先に更新するときに使う。
15
+ */
16
+ export declare function mergePatch<T extends object>(record: T, patch: MergePatch): T;
17
+ /**
18
+ * ドラッグで動かしたノードの位置を保存する patch。
19
+ * 保存してある position に移動量を足し、整数に丸める(position が無いときは 0, 0 から)。
20
+ * 子のノードの position は親の中での位置なので、親を動かしても子の patch は要らない。
21
+ */
22
+ export declare function movePatch(node: UnchiNodeRecord, move: UnchiMove): UnchiChange;
23
+ /**
24
+ * 色として読める文字列か。
25
+ * ブラウザでは CSS.supports('color', v) で判定する。Node など CSS の無い環境では、
26
+ * #rgb / #rrggbb / #rrggbbaa・rgb() などの関数表記・英字だけの色名、を通す緩い判定にする
27
+ * (色名の一覧は持たない。環境で判定が変わる点は README に書く)。
28
+ */
29
+ export declare function isColor(value: string): boolean;
30
+ /**
31
+ * インスペクタの「適用」の値を、保存する patch に変換する。
32
+ * 図に描いている値から変えた項目だけを patch に入れる(変えていない既定の値まで書き込まないため)。
33
+ * 変更が無ければ patch は {}。
34
+ */
35
+ export declare function applyPatch(record: UnchiRecord, values: UnchiValues): UnchiChange;
36
+ //# sourceMappingURL=patch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../src/patch.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,UAAU,EAAE,WAAW,EAAE,eAAe,EAAmB,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAChH,MAAM,YAAY,CAAC;AAEpB,8BAA8B;AAC9B,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B;AAED,oBAAoB;AACpB,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,eAAe,CAEjE;AAED,oCAAoC;AACpC,wBAAgB,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAQnD;AAED,2BAA2B;AAC3B,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAK7D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,UAAU,GAAG,CAAC,CAmB5E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,GAAG,WAAW,CAa7E;AAWD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAY9C;AAiKD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAG,WAAW,CAMhF"}