VSCodeでSVGを編集できるやつ2.0(SVG Editor)

# これは何

VSCode上でSVGを画像編集ソフトっぽく編集したくて作っている拡張です。[Marketplaceのページ。](https://marketplace.visualstudio.com/items?itemName=henoc.svgeditor)

*2018/9/2 少し追記*

![capture.png](https://qiita-image-store.s3.amazonaws.com/0/33856/eb622c3e-dc1b-0329-7f30-5e366e465c9c.png)

右側のウィンドウで図形を選択して動かしたりできます。この度Elmで書いていたのをすべてTypeScriptで書き直したので苦労話とかをまとめてみました。使って～

## なぜElmをやめたの？

portが辛かった...辛かったんです...許して...

まあ実際のdom要素から取得しないといけない値とか結構あったので、だんだんきつくなっていったんですね。

## はまった点

- cssでwidthとかheightを指定するときは`px`とか単位が必須、SVGと同じ気分で書いてはいけない
- SVGの要素は`document.createElementNS`で名前空間を指定する
- 単位換算
    - [units-css](https://www.npmjs.com/package/units-css)という便利ライブラリがあるがSVGにそのまま適用はできない
    - %の計算にライブラリでは`offsetWidth`, `offsetHeight`を使って要素の幅や高さを測っているがSVG要素にはそのプロパティはない...
    - あとex, emの計算がライブラリは何かおかしい
    - ので`getComputedStyle`しまくる
- viewBoxが難しい（つらい）多分負の値でまだバグがある
- ネストされたsvg要素の当たり判定がそれの内部にある要素以上に拡大されない（ので透明のrectをこっそり挿入）
- transform属性は頭が痛い。g要素と絡むともっと頭が痛い。
    - inkscapeはすごい
- text要素の拡大とか
    - 「文字列の高さ(lineHeight)→フォントサイズ」を求める方法がない。逆はできる（フォントの属性を与えてlineHeightとかbaselineとかを測ってくれるライブラリは[font-measure](https://www.npmjs.com/package/font-measure)が優秀）
    - 仕方ないので二分探索
- 使用可能なフォントのリストが取れない（指定したフォントが使用可能かを判定することはできるらしい...: [font-detect.js](https://github.com/zenozeng/font-detect.js)）。仕方ないのでOSごとにフォントが入っていそうなディレクトリを探索して(参考: [system-font-families](https://github.com/rBurgett/system-font-families))フォントファイルのバイナリを読み込んでfontFamilyをもらってくる。始めは[opentype.js](https://opentype.js.org/)で読み込んでいたがファイルごとにすべてパースされるのが遅いので自力に切り替える(参考: [ttfinfo](https://www.npmjs.com/package/ttfinfo))
- WebViewAPIでlocalRootResourcesに許可をとってもSVGのuseタグのhrefで外部SVGリソースを参照するとはじかれる。APIのバグな気がする。htmlに埋め込むことで回避。

## 優秀なライブラリたち

- [font-measure](https://www.npmjs.com/package/font-measure) フォントの属性を与えるといろいろな高さを測ってくれる。多分canvasで頑張っている。
- [incremental-dom](https://www.npmjs.com/package/incremental-dom) vdomライブラリ。React、Vueは軟弱。ちゃんとSVGにも使える。
- [svgpath](https://www.npmjs.com/package/svgpath) path要素のd属性の値をパースしていろいろいじってくれる感動ライブラリ。
- [transform-matrix](https://www.npmjs.com/package/transformation-matrix) transform属性のパース、行列計算のライブラリ。これも頼もしい。
- [tinycolor2](https://www.npmjs.com/package/tinycolor2) cssの色文字列を何でもパースしてくれる。

## VSCodeの拡張を書いて得た知識

- 最近できた[Webview API](https://code.visualstudio.com/docs/extensions/webview)で色々便利になった。適用されているテーマの色をcss変数で取得できるところとか素敵。
- untitledファイルを開く関数は`vscode.workspace.openTextDocument`。以下。

```typescript
function workspace.openTextDocument(options?: {
    language?: string | undefined;
    content?: string | undefined;
} | undefined): Thenable<vscode.TextDocument>
```

## おまけ Webview APIで継承されるcss変数一覧

リンクの色ってテーマで指定できないんですね。

```css
:root {
    --background-color: #1e1e1e;
    --color: #d4d4d4;
    --font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, "Noto Sans", Meiryo, "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif;
    --font-size: 13px;
    --font-weight: normal;
    --link-active-color: #4080d0;
    --link-color: #4080d0;
    --vscode-activityBar-background: #333333;
    --vscode-activityBar-dropBackground: rgba(255, 255, 255, 0.12);
    --vscode-activityBar-foreground: #ffffff;
    --vscode-activityBarBadge-background: #007acc;
    --vscode-activityBarBadge-foreground: #ffffff;
    --vscode-badge-background: #4d4d4d;
    --vscode-badge-foreground: #ffffff;
    --vscode-button-background: #0e639c;
    --vscode-button-foreground: #ffffff;
    --vscode-button-hoverBackground: #1177bb;
    --vscode-debugExceptionWidget-background: #420b0d;
    --vscode-debugExceptionWidget-border: #a31515;
    --vscode-debugToolBar-background: #333333;
    --vscode-descriptionForeground: rgba(204, 204, 204, 0.7);
    --vscode-diffEditor-insertedTextBackground: rgba(155, 185, 85, 0.2);
    --vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 0.2);
    --vscode-dropdown-background: #3c3c3c;
    --vscode-dropdown-border: #3c3c3c;
    --vscode-dropdown-foreground: #f0f0f0;
    --vscode-editor-background: #1e1e1e;
    --vscode-editor-findMatchBackground: #515c6a;
    --vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 0.33);
    --vscode-editor-findRangeHighlightBackground: rgba(58, 61, 65, 0.4);
    --vscode-editor-font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, "Noto Sans", Meiryo, "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif;
    --vscode-editor-font-size: 13px;
    --vscode-editor-font-weight: normal;
    --vscode-editor-foreground: #d4d4d4;
    --vscode-editor-hoverHighlightBackground: rgba(38, 79, 120, 0.25);
    --vscode-editor-inactiveSelectionBackground: #3a3d41;
    --vscode-editor-lineHighlightBorder: #282828;
    --vscode-editor-rangeHighlightBackground: rgba(255, 255, 255, 0.04);
    --vscode-editor-selectionBackground: #264f78;
    --vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 0.15);
    --vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 0.72);
    --vscode-editor-wordHighlightStrongBackground: rgba(0, 73, 114, 0.72);
    --vscode-editorActiveLineNumber-foreground: #aaaaaa;
    --vscode-editorBracketMatch-background: rgba(0, 100, 0, 0.1);
    --vscode-editorBracketMatch-border: #888888;
    --vscode-editorCodeLens-foreground: #999999;
    --vscode-editorCursor-foreground: #aeafad;
    --vscode-editorError-foreground: #ea4646;
    --vscode-editorGroup-border: #444444;
    --vscode-editorGroup-dropBackground: rgba(83, 89, 93, 0.5);
    --vscode-editorGroupHeader-noTabsBackground: #1e1e1e;
    --vscode-editorGroupHeader-tabsBackground: #252526;
    --vscode-editorGutter-addedBackground: #587c0c;
    --vscode-editorGutter-background: #1e1e1e;
    --vscode-editorGutter-deletedBackground: #94151b;
    --vscode-editorGutter-modifiedBackground: #0c7d9d;
    --vscode-editorHint-foreground: rgba(238, 238, 238, 0.7);
    --vscode-editorHoverWidget-background: #2d2d30;
    --vscode-editorHoverWidget-border: #454545;
    --vscode-editorIndentGuide-activeBackground: #707070;
    --vscode-editorIndentGuide-background: #404040;
    --vscode-editorInfo-foreground: #008000;
    --vscode-editorLineNumber-activeForeground: #aaaaaa;
    --vscode-editorLineNumber-foreground: #5a5a5a;
    --vscode-editorLink-activeForeground: #4e94ce;
    --vscode-editorMarkerNavigation-background: #2d2d30;
    --vscode-editorMarkerNavigationError-background: #ea4646;
    --vscode-editorMarkerNavigationInfo-background: #008000;
    --vscode-editorMarkerNavigationWarning-background: #4d9e4d;
    --vscode-editorOverviewRuler-addedForeground: rgba(0, 122, 204, 0.6);
    --vscode-editorOverviewRuler-border: rgba(127, 127, 127, 0.3);
    --vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0;
    --vscode-editorOverviewRuler-commonContentForeground: rgba(96, 96, 96, 0.4);
    --vscode-editorOverviewRuler-currentContentForeground: rgba(64, 200, 174, 0.5);
    --vscode-editorOverviewRuler-deletedForeground: rgba(0, 122, 204, 0.6);
    --vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 0.7);
    --vscode-editorOverviewRuler-findMatchForeground: rgba(246, 185, 77, 0.7);
    --vscode-editorOverviewRuler-incomingContentForeground: rgba(64, 166, 255, 0.5);
    --vscode-editorOverviewRuler-infoForeground: rgba(18, 18, 136, 0.7);
    --vscode-editorOverviewRuler-modifiedForeground: rgba(0, 122, 204, 0.6);
    --vscode-editorOverviewRuler-rangeHighlightForeground: rgba(0, 122, 204, 0.6);
    --vscode-editorOverviewRuler-selectionHighlightForeground: rgba(160, 160, 160, 0.8);
    --vscode-editorOverviewRuler-warningForeground: rgba(18, 136, 18, 0.7);
    --vscode-editorOverviewRuler-wordHighlightForeground: rgba(160, 160, 160, 0.8);
    --vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba(192, 160, 192, 0.8);
    --vscode-editorPane-background: #1e1e1e;
    --vscode-editorRuler-foreground: #5a5a5a;
    --vscode-editorSuggestWidget-background: #2d2d30;
    --vscode-editorSuggestWidget-border: #454545;
    --vscode-editorSuggestWidget-foreground: #d4d4d4;
    --vscode-editorSuggestWidget-highlightForeground: #0097fb;
    --vscode-editorSuggestWidget-selectedBackground: #073655;
    --vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 0.67);
    --vscode-editorWarning-foreground: #4d9e4d;
    --vscode-editorWhitespace-foreground: rgba(227, 228, 226, 0.16);
    --vscode-editorWidget-background: #2d2d30;
    --vscode-editorWidget-border: #454545;
    --vscode-errorForeground: #f48771;
    --vscode-extensionButton-prominentBackground: #327e36;
    --vscode-extensionButton-prominentForeground: #ffffff;
    --vscode-extensionButton-prominentHoverBackground: #28632b;
    --vscode-focusBorder: rgba(14, 99, 156, 0.6);
    --vscode-foreground: #cccccc;
    --vscode-gitDecoration-addedResourceForeground: #81b88b;
    --vscode-gitDecoration-conflictingResourceForeground: #6c6cc4;
    --vscode-gitDecoration-deletedResourceForeground: #c74e39;
    --vscode-gitDecoration-ignoredResourceForeground: #a7a8a9;
    --vscode-gitDecoration-modifiedResourceForeground: #e2c08d;
    --vscode-gitDecoration-submoduleResourceForeground: #8db9e2;
    --vscode-gitDecoration-untrackedResourceForeground: #73c991;
    --vscode-input-background: #3c3c3c;
    --vscode-input-foreground: #cccccc;
    --vscode-inputOption-activeBorder: #007acc;
    --vscode-inputValidation-errorBackground: #5a1d1d;
    --vscode-inputValidation-errorBorder: #be1100;
    --vscode-inputValidation-infoBackground: #063b49;
    --vscode-inputValidation-infoBorder: #007acc;
    --vscode-inputValidation-warningBackground: #352a05;
    --vscode-inputValidation-warningBorder: #b89500;
    --vscode-list-activeSelectionBackground: #094771;
    --vscode-list-activeSelectionForeground: #ffffff;
    --vscode-list-dropBackground: #383b3d;
    --vscode-list-errorForeground: #ea4646;
    --vscode-list-focusBackground: #073655;
    --vscode-list-highlightForeground: #0097fb;
    --vscode-list-hoverBackground: #2a2d2e;
    --vscode-list-inactiveFocusBackground: #313135;
    --vscode-list-inactiveSelectionBackground: #3f3f46;
    --vscode-list-invalidItemForeground: #b89500;
    --vscode-list-warningForeground: #4d9e4d;
    --vscode-merge-commonContentBackground: rgba(96, 96, 96, 0.16);
    --vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 0.4);
    --vscode-merge-currentContentBackground: rgba(64, 200, 174, 0.2);
    --vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 0.5);
    --vscode-merge-incomingContentBackground: rgba(64, 166, 255, 0.2);
    --vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 0.5);
    --vscode-notificationCenterHeader-background: #3b3b3e;
    --vscode-notificationLink-foreground: #4080d0;
    --vscode-notifications-background: #2d2d30;
    --vscode-notifications-border: #3b3b3e;
    --vscode-panel-background: #1e1e1e;
    --vscode-panel-border: rgba(128, 128, 128, 0.35);
    --vscode-panel-dropBackground: rgba(255, 255, 255, 0.12);
    --vscode-panelTitle-activeBorder: rgba(128, 128, 128, 0.35);
    --vscode-panelTitle-activeForeground: #e7e7e7;
    --vscode-panelTitle-inactiveForeground: rgba(231, 231, 231, 0.5);
    --vscode-peekView-border: #007acc;
    --vscode-peekViewEditor-background: #001f33;
    --vscode-peekViewEditor-matchHighlightBackground: rgba(255, 143, 0, 0.6);
    --vscode-peekViewEditorGutter-background: #001f33;
    --vscode-peekViewResult-background: #252526;
    --vscode-peekViewResult-fileForeground: #ffffff;
    --vscode-peekViewResult-lineForeground: #bbbbbb;
    --vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 0.3);
    --vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 0.2);
    --vscode-peekViewResult-selectionForeground: #ffffff;
    --vscode-peekViewTitle-background: #1e1e1e;
    --vscode-peekViewTitleDescription-foreground: rgba(204, 204, 204, 0.7);
    --vscode-peekViewTitleLabel-foreground: #ffffff;
    --vscode-pickerGroup-border: #3f3f46;
    --vscode-pickerGroup-foreground: rgba(0, 151, 251, 0.6);
    --vscode-progressBar-background: #0e70c0;
    --vscode-scrollbar-shadow: #000000;
    --vscode-scrollbarSlider-activeBackground: rgba(191, 191, 191, 0.4);
    --vscode-scrollbarSlider-background: rgba(121, 121, 121, 0.4);
    --vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7);
    --vscode-settings-modifiedItemForeground: #73c991;
    --vscode-sideBar-background: #252526;
    --vscode-sideBar-dropBackground: rgba(255, 255, 255, 0.12);
    --vscode-sideBarSectionHeader-background: rgba(128, 128, 128, 0.2);
    --vscode-sideBarTitle-foreground: #bbbbbb;
    --vscode-statusBar-background: #007acc;
    --vscode-statusBar-debuggingBackground: #cc6633;
    --vscode-statusBar-debuggingForeground: #ffffff;
    --vscode-statusBar-foreground: #ffffff;
    --vscode-statusBar-noFolderBackground: #68217a;
    --vscode-statusBar-noFolderForeground: #ffffff;
    --vscode-statusBarItem-activeBackground: rgba(255, 255, 255, 0.18);
    --vscode-statusBarItem-hoverBackground: rgba(255, 255, 255, 0.12);
    --vscode-statusBarItem-prominentBackground: #388a34;
    --vscode-statusBarItem-prominentHoverBackground: #369432;
    --vscode-tab-activeBackground: #1e1e1e;
    --vscode-tab-activeForeground: #ffffff;
    --vscode-tab-border: #252526;
    --vscode-tab-inactiveBackground: #2d2d2d;
    --vscode-tab-inactiveForeground: rgba(255, 255, 255, 0.5);
    --vscode-tab-unfocusedActiveForeground: rgba(255, 255, 255, 0.5);
    --vscode-tab-unfocusedInactiveForeground: rgba(255, 255, 255, 0.25);
    --vscode-terminal-ansiBlack: #000000;
    --vscode-terminal-ansiBlue: #2472c8;
    --vscode-terminal-ansiBrightBlack: #666666;
    --vscode-terminal-ansiBrightBlue: #3b8eea;
    --vscode-terminal-ansiBrightCyan: #29b8db;
    --vscode-terminal-ansiBrightGreen: #23d18b;
    --vscode-terminal-ansiBrightMagenta: #d670d6;
    --vscode-terminal-ansiBrightRed: #f14c4c;
    --vscode-terminal-ansiBrightWhite: #e5e5e5;
    --vscode-terminal-ansiBrightYellow: #f5f543;
    --vscode-terminal-ansiCyan: #11a8cd;
    --vscode-terminal-ansiGreen: #0dbc79;
    --vscode-terminal-ansiMagenta: #bc3fbc;
    --vscode-terminal-ansiRed: #cd3131;
    --vscode-terminal-ansiWhite: #e5e5e5;
    --vscode-terminal-ansiYellow: #e5e510;
    --vscode-terminal-foreground: #cccccc;
    --vscode-terminal-selectionBackground: rgba(255, 255, 255, 0.25);
    --vscode-textBlockQuote-background: rgba(127, 127, 127, 0.1);
    --vscode-textBlockQuote-border: rgba(0, 122, 204, 0.5);
    --vscode-textCodeBlock-background: rgba(10, 10, 10, 0.4);
    --vscode-textLink-activeForeground: #4080d0;
    --vscode-textLink-foreground: #4080d0;
    --vscode-textPreformat-foreground: #d7ba7d;
    --vscode-textSeparator-foreground: rgba(255, 255, 255, 0.18);
    --vscode-titleBar-activeBackground: #3c3c3c;
    --vscode-titleBar-activeForeground: #cccccc;
    --vscode-titleBar-inactiveBackground: rgba(60, 60, 60, 0.6);
    --vscode-titleBar-inactiveForeground: rgba(204, 204, 204, 0.6);
    --vscode-widget-shadow: #000000;
}
```