From 12bff625fdc9c2efd72fb0f006d1b885ebf1fb23 Mon Sep 17 00:00:00 2001 From: Palf Date: Sat, 18 Apr 2026 12:59:58 +0900 Subject: [PATCH 1/7] =?UTF-8?q?Revert=20"feat:=20=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC=E3=82=92Edit=E5=86=85?= =?UTF-8?q?=E3=81=AEMaster=20Grid=E7=9B=B4=E5=BE=8C=E3=81=AB=E6=8C=BF?= =?UTF-8?q?=E5=85=A5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit dc0d3aa0394e310e9324c94f0041a807441fd24c. --- .../Contents/Resources/plugin.py | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py index 424417f..a7ed670 100644 --- a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py +++ b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py @@ -11,7 +11,6 @@ NSTextField, NSStepper, NSButton, NSColorWell, NSBundle, NSNib, NSAttributedString, NSFont, NSForegroundColorAttributeName, NSFontAttributeName, - NSApp, ) from Foundation import NSObject, NSSelectorFromString @@ -536,24 +535,7 @@ def start(self): settingsItem = NSMenuItem(settingsLabel, callback=self._showSettings_, target=self) else: settingsItem = NSMenuItem(settingsLabel, self._showSettings_) - inserted = False - try: - main_menu = NSApp.mainMenu() - for top_item in main_menu.itemArray(): - submenu = top_item.submenu() - if submenu is None: - continue - for i, item in enumerate(submenu.itemArray()): - if 'Master Grid' in (item.title() or ''): - submenu.insertItem_atIndex_(settingsItem, i + 1) - inserted = True - break - if inserted: - break - except Exception: - pass - if not inserted: - Glyphs.menu[EDIT_MENU].append(settingsItem) + Glyphs.menu[EDIT_MENU].append(settingsItem) Glyphs.addCallback(self._drawGrid_, DRAWBACKGROUND) Glyphs.addCallback(self._snapDuringDrag_, MOUSEDRAGGED) From deb1f14ad9b1ca31bfdac6de6d2df79e9ca796b2 Mon Sep 17 00:00:00 2001 From: Palf Date: Sat, 18 Apr 2026 13:07:37 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E8=A8=AD=E5=AE=9A=E3=83=A1?= =?UTF-8?q?=E3=83=8B=E3=83=A5=E3=83=BC=E3=82=92Edit=E5=86=85=E3=81=AEPaste?= =?UTF-8?q?=E7=9B=B4=E5=BE=8C=E3=81=AB=E6=8C=BF=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contents/Resources/plugin.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py index a7ed670..50aa9c2 100644 --- a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py +++ b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py @@ -11,6 +11,7 @@ NSTextField, NSStepper, NSButton, NSColorWell, NSBundle, NSNib, NSAttributedString, NSFont, NSForegroundColorAttributeName, NSFontAttributeName, + NSApp, ) from Foundation import NSObject, NSSelectorFromString @@ -535,7 +536,24 @@ def start(self): settingsItem = NSMenuItem(settingsLabel, callback=self._showSettings_, target=self) else: settingsItem = NSMenuItem(settingsLabel, self._showSettings_) - Glyphs.menu[EDIT_MENU].append(settingsItem) + inserted = False + try: + main_menu = NSApp.mainMenu() + for top_item in main_menu.itemArray(): + submenu = top_item.submenu() + if submenu is None: + continue + for i, item in enumerate(submenu.itemArray()): + if (item.title() or '') == 'Paste': + submenu.insertItem_atIndex_(settingsItem, i + 1) + inserted = True + break + if inserted: + break + except Exception: + pass + if not inserted: + Glyphs.menu[EDIT_MENU].append(settingsItem) Glyphs.addCallback(self._drawGrid_, DRAWBACKGROUND) Glyphs.addCallback(self._snapDuringDrag_, MOUSEDRAGGED) From a5ea76e395699edb628de6a69259d7e04dd63759 Mon Sep 17 00:00:00 2001 From: Palf Date: Thu, 30 Apr 2026 11:01:15 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=E3=82=B0=E3=83=AA=E3=83=83?= =?UTF-8?q?=E3=83=89=E8=A1=A8=E7=A4=BA=E4=B8=AD=E3=81=AE=E7=9F=A2=E5=8D=B0?= =?UTF-8?q?=E3=82=AD=E3=83=BC=E7=A7=BB=E5=8B=95=E9=87=8F=E3=82=92=E3=82=B5?= =?UTF-8?q?=E3=83=96/=E3=83=A1=E3=82=A4=E3=83=B3=E3=82=B0=E3=83=AA?= =?UTF-8?q?=E3=83=83=E3=83=89=E3=81=AB=E5=89=B2=E3=82=8A=E5=BD=93=E3=81=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NSEvent のローカルモニタで keyDown を横取りし、グリッド表示中は 矢印キーをサブグリッド間隔、Shift+矢印キーをメイングリッド間隔の 移動に差し替える。Cmd/Option/Control 併用時、設定パネルキー時、 テキスト編集中、ノード未選択時は素通しする。オンカーブノードに 付随するオフカーブハンドルは _snapDuringDrag_ と同じロジックで 追従させる。 Co-Authored-By: Claude Opus 4.7 --- .../Contents/Resources/plugin.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py index c10674c..65f54cd 100644 --- a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py +++ b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py @@ -12,6 +12,7 @@ NSTextField, NSStepper, NSButton, NSColorWell, NSBundle, NSNib, NSAttributedString, NSFont, NSForegroundColorAttributeName, NSFontAttributeName, + NSEvent, ) from Foundation import NSObject, NSSelectorFromString @@ -553,6 +554,8 @@ def start(self): Glyphs.addCallback(self._snapDuringDrag_, MOUSEDRAGGED) Glyphs.addCallback(self._snapDuringDrag_, MOUSEUP) + self._installArrowKeyMonitor() + def _toggleGrid_(self, sender): self.gridVisible = not self.gridVisible self._showItem.setState_(1 if self.gridVisible else 0) @@ -658,6 +661,110 @@ def _snapDuringDrag_(self, notification): except Exception: print(traceback.format_exc()) + @objc.python_method + def _installArrowKeyMonitor(self): + """グリッド表示中、矢印キーをサブグリッド単位、Shift+矢印キーをメイングリッド単位の移動に差し替える。""" + NSEventMaskKeyDown = 1 << 10 + + def handler(event): + try: + return self._handleArrowKeyEvent(event) + except Exception: + print(traceback.format_exc()) + return event + + self._arrowKeyMonitor = NSEvent.addLocalMonitorForEventsMatchingMask_handler_( + NSEventMaskKeyDown, handler + ) + + @objc.python_method + def _handleArrowKeyEvent(self, event): + if not self.gridVisible: + return event + + keyCode = event.keyCode() + # 123=Left, 124=Right, 125=Down, 126=Up + if keyCode not in (123, 124, 125, 126): + return event + + SHIFT, CONTROL, OPTION, COMMAND = 1 << 17, 1 << 18, 1 << 19, 1 << 20 + flags = event.modifierFlags() & 0xFFFF0000 + # Shift 単独以外の修飾キー(Cmd/Option/Control)は Glyphs の既定動作に委ねる + if flags & (CONTROL | OPTION | COMMAND): + return event + + window = NSApplication.sharedApplication().keyWindow() + if window is None: + return event + # 設定パネルがキーウィンドウのときは横取りしない + sc = getattr(self, '_settingsController', None) + if sc is not None: + panel = getattr(sc, 'panel', None) + if panel is not None and window is panel: + return event + # テキスト編集中(フィールドエディタ)は横取りしない + responder = window.firstResponder() + if responder is not None and responder.className() == 'NSTextView': + return event + + font = Glyphs.font + if not font: + return event + layers = font.selectedLayers + if not layers: + return event + layer = layers[0] + + selectedNodes = [n for n in layer.selection + if hasattr(n, 'position') and hasattr(n, 'type')] + if not selectedNodes: + return event + + s = self._getSettings(font) + mainX, mainY = self._mainIntervals(layer, s) + if mainX <= 0 or mainY <= 0: + return event + + if flags & SHIFT: + stepX, stepY = mainX, mainY + else: + stepX, stepY = self._subIntervals(s, mainX, mainY) + if stepX <= 0 or stepY <= 0: + stepX, stepY = mainX, mainY + + dx, dy = 0.0, 0.0 + if keyCode == 123: + dx = -stepX + elif keyCode == 124: + dx = stepX + elif keyCode == 125: + dy = -stepY + elif keyCode == 126: + dy = stepY + + selectedSet = set(selectedNodes) + moves = {id(n): (n, dx, dy) for n in selectedNodes} + # オンカーブノードに付随するオフカーブハンドルも一緒に動かす + for nid, (node, ddx, ddy) in list(moves.items()): + if node.type == OFFCURVE: + continue + for neighbor in (node.prevNode, node.nextNode): + if neighbor is None: + continue + if neighbor.type != OFFCURVE: + continue + if neighbor in selectedSet: + continue + if id(neighbor) in moves: + continue + moves[id(neighbor)] = (neighbor, ddx, ddy) + + for node, ddx, ddy in moves.values(): + p = node.position + node.position = NSPoint(p.x + ddx, p.y + ddy) + + return None + @objc.python_method def _drawGrid_(self, layer, info): if not self.gridVisible: From c1289340e34cd9562936ade01f513232d43cfc16 Mon Sep 17 00:00:00 2001 From: Palf Date: Sun, 28 Jun 2026 19:15:19 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=E3=82=B0=E3=83=AA=E3=83=83?= =?UTF-8?q?=E3=83=89=E9=96=93=E9=9A=94=EF=BC=88gap=EF=BC=89=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=81=A8=E3=83=AA=E3=82=A2=E3=83=AB=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=A0=E3=83=97=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 四角・三角両グリッドに対してメイン/サブ各間隔値をH/V・Main/Sub 独立設定・リンク切替で管理できるグリッド間隔(gap)機能を追加 - gap > 0 の場合、1本の線を法線方向に ±gap/2 ずらした2本に分離して描画 - 三角グリッドでは全3系統(水平/斜め2方向)をギャップ考慮で描画し、 スナップもずらした線の交点に吸着するよう対応 - 設定パネルで値を変更するたびにグリッドをリアルタイムプレビュー( キャンセルで確定前の状態に戻る) - 三角形モード時は H 欄のみ有効、V・Sync H/V は無効化 Co-authored-by: Cursor --- .../Resources/SnappingGridSettings.nib | Bin 23552 -> 29220 bytes .../Resources/SnappingGridSettings.xib | 180 ++++-- .../Contents/Resources/plugin.py | 523 ++++++++++++++++-- 3 files changed, 621 insertions(+), 82 deletions(-) diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.nib b/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.nib index 45cfe5c321092bfa8e9627ba262cedea02d3e003..f43d85383e41ffe2c8f988217a235fa73811784a 100644 GIT binary patch delta 8249 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

tM^l+9&4THWncEdKs z)T6t!N4LL6cc4diut#^ON7vM&JKUo)^(dF=dK3=KzyefY1vX#@4&VeX&VN0a5Fdw MZqfCs8xJY}1hV-w-2eap delta 4501 zcmchZX?RrS8HUdZBn$_#0D+J_AwU8In3?PWB`gVH53*#>ge+tw2_cD+0OL33o8`<* zmdQsTi3_4uZQamN<+9egw6<8|vr1Zqc#N1tTAUwdto zf6Dl0jDOCY*KGVN=K7j*i7~g#IQM6a2jDz|S$k(QwJ(^l{t!0AmoPT4l=bmc#$0Jk z<(Z6mvap+t`5Md%FyD>2i7~B`jq+~BoIT9OWee-~vkdVt3maHy>Eq_5Xe6Ss7L6Ow z*k)ma+btePxrOlxV6rf0rNz$cEX-AJQQb`z&RZ?a-Da`6J8?dWbGOB+nJuF{TxHG( z)yCsg)*r79@pP39WT<^S7j+)$Qq*Or8&x*gq}koMCo5kFhfmYaiuVc6Kt`Zq?S|T#Q+X z-KJII+>LXO-7fqbqddyNPDYy@*8W9~A)eu21DTG#!90h@QRra2$ibY&4!c%~#crH? z9QfO5Y{*YL?FiSHHA1uSD2?%Gjk#kqix#Ji@@$Pc*Jw7r9(x@YVk2#FJcFo>@R2$;vASW9LcJ}dTC+m-K4)GM!sm@-$9`y#)g{X^A*P=d%x(D?! z>?EORmQ zsd5|+pizZ9h^#|4A)AqH$iv7^WEZj<*@HAAB{%2O-RzV7|b3iBvLoWioNStGO)#3QwifU2oMRPKs1N}u^`UF z9Pu7DyvQ?lP4~u(WnRp+M} zQ!>S4sY}Jjt2$((s9yatI_cuuv`d}640GB0x0ov7XZ zZvS!Rb}^D$DBmTnu303X70<3Th}^umz`HS@yv`ZXp0_ygUb@Z?=sNdZzs~(ruJdNz zx*0#D`H#dW%i`n*Mac4SQNDSj{4n+x$d8Cylb48fTl~d4o1+3B#pYW|EPP3MNbJrw zh_(3<@?*65xM<1G4;-Tn6K$LmZCh4|kMl$2AJfJY*zjI!ap03vw$5WKM?ADHNu0=^ zFFwNbCp7ilRP<^EQ6dqBY5j9&43x9_5r#ibnP9JB8 zGd!bXhGA*O-O96L4{t4+jZaKK;r@(YVrN{rNd7hXH{@~h@5z58zf69W{3o%aI;`q# zI{%G)h5R1*ee#FokI0{puaZ9_UnBpA{3ZD-@;Bt~$g(Wnu20$;gnyXhN(gx_Ih4GB z97bM94kt&FqhOEXhYMX+y#Lq+m0wBXgQ6&1H5D&OqO3SdXQkqjBrX+4>T;;~tt5I% z68wHAiMvW-bVJnpgCt%=Q9#9INraRp_`M>Dw9+`;8S1?ziSp9uWbc1y2bI_1*Ji4^ zUQ_dds@mo~omHFL8)_z>%n0qgA&HBn(dH5=-kNxT&2;EMLrebTW+e^(Ix%$mhI=|{ z+a?zd(ZV|u3mPr#IoeV^Ij^JnyAyNY)%Q;h8_}LVsQg_LSIfeaF3{>fCRR05Ytx3# zj*ixr$?Z)v|3DJP@&vzsN}{lQiQmVPI8h$&_b*ADMe|de1pmtl2~04uJe8)4l4hZL_tNA`2-bT!`?T{YC6(cb^YaAX!h;IOrzP< zSKmi-34dqrV;$c4)EDodvERg)E`9yAyJ$9D7OkcjIY9K8BI9(E*Qu#(uB|)JQR|_P z&1L!s4JwMbXo{3)iz}vt?X&5l<$cE{r@`o`I<>Va+oO2jsmcBv>bnoM*3@pSKhRQF zTZ4raQx@ily_Jdbd~vKYK4U&zHh;?0`*4r4dP-*=b-Zux+AnzvMMBYQ*OWsG`Pc9@Ek&DSCW{OgB8M&NXK{k;q$p^?) + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + @@ -155,19 +173,19 @@ - + - + - + @@ -175,35 +193,35 @@ - + - + - + - + - + @@ -211,19 +229,19 @@ - + - + - + @@ -231,14 +249,114 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py index 65f54cd..3b49814 100644 --- a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py +++ b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py @@ -51,6 +51,24 @@ class SettingsPanelController(NSObject): _radioHorizontal = objc.IBOutlet() _radioVertical = objc.IBOutlet() _resetButton = objc.IBOutlet() + _labelGridGap = objc.IBOutlet() + _gapEnable = objc.IBOutlet() + _gapSyncMainSub = objc.IBOutlet() + _labelMainGap = objc.IBOutlet() + _captionMainGapH = objc.IBOutlet() + _captionMainGapV = objc.IBOutlet() + _mainGapH = objc.IBOutlet() + _mainGapV = objc.IBOutlet() + _mainGapHStep = objc.IBOutlet() + _mainGapVStep = objc.IBOutlet() + _gapSyncHV = objc.IBOutlet() + _labelSubGap = objc.IBOutlet() + _captionSubGapH = objc.IBOutlet() + _captionSubGapV = objc.IBOutlet() + _subGapH = objc.IBOutlet() + _subGapV = objc.IBOutlet() + _subGapHStep = objc.IBOutlet() + _subGapVStep = objc.IBOutlet() def initWithPlugin_(self, plugin): self = objc.super(SettingsPanelController, self).init() @@ -64,6 +82,7 @@ def initWithPlugin_(self, plugin): def show(self): if self.panel is None: self._loadNib() + self.plugin._clearPreview() self._loadToUI() self.panel.makeKeyAndOrderFront_(None) @@ -99,17 +118,29 @@ def _wireControls(self): step.setValueWraps_(False) step.setTarget_(self) step.setAction_(step_action) - for field in (self._mainH, self._mainV, self._subH, self._subV): + for step in (self._mainGapHStep, self._mainGapVStep, self._subGapHStep, self._subGapVStep): + step.setMinValue_(0) + step.setMaxValue_(999) + step.setIncrement_(1) + step.setValueWraps_(False) + step.setTarget_(self) + step.setAction_(step_action) + for field in (self._mainH, self._mainV, self._subH, self._subV, + self._mainGapH, self._mainGapV, self._subGapH, self._subGapV): field.setEditable_(True) field.setSelectable_(True) field.setBezeled_(True) field.setDrawsBackground_(True) field.setDelegate_(self) self._stepperPairs = ( - (self._mainHStep, self._mainH), - (self._mainVStep, self._mainV), - (self._subHStep, self._subH), - (self._subVStep, self._subV), + (self._mainHStep, self._mainH, 1, 'mainGrid'), + (self._mainVStep, self._mainV, 1, 'mainGrid'), + (self._subHStep, self._subH, 1, 'subGrid'), + (self._subVStep, self._subV, 1, 'subGrid'), + (self._mainGapHStep, self._mainGapH, 0, 'mainGap'), + (self._mainGapVStep, self._mainGapV, 0, 'mainGap'), + (self._subGapHStep, self._subGapH, 0, 'subGap'), + (self._subGapVStep, self._subGapV, 0, 'subGap'), ) mode_sel = NSSelectorFromString('modeChanged:') self._radioDivision.setTarget_(self) @@ -134,6 +165,18 @@ def _wireControls(self): reset_sel = NSSelectorFromString('resetToDefaults:') self._resetButton.setTarget_(self) self._resetButton.setAction_(reset_sel) + gap_sel = NSSelectorFromString('gapChanged:') + for button in (self._gapEnable, self._gapSyncMainSub, self._gapSyncHV): + button.setTarget_(self) + button.setAction_(gap_sel) + preview_sel = NSSelectorFromString('previewChanged:') + self._snapCheck.setTarget_(self) + self._snapCheck.setAction_(preview_sel) + colour_sel = NSSelectorFromString('colourChanged:') + for well in (self._mainColorWell, self._subColorWell): + well.setContinuous_(True) + well.setTarget_(self) + well.setAction_(colour_sel) def _applyLocalisation(self): lx = Glyphs.localize @@ -181,6 +224,17 @@ def _applyLocalisation(self): })) self._radioHorizontal.setTitle_(lx({'en': 'Horizontal', 'ja': '水平', 'zh': '水平', 'ko': '수평'})) self._radioVertical.setTitle_(lx({'en': 'Vertical', 'ja': '垂直', 'zh': '垂直', 'ko': '수직'})) + self._labelGridGap.setStringValue_(lx({ + 'en': 'Grid Gap:', + 'ja': 'グリッド間隔:', + 'zh': '网格间隔:', + 'ko': '그리드 간격:', + })) + self._gapEnable.setTitle_(lx({'en': 'Enable', 'ja': '有効にする', 'zh': '启用', 'ko': '활성화'})) + self._gapSyncMainSub.setTitle_(lx({'en': 'Link Main/Sub', 'ja': 'メイン/サブ連動', 'zh': '关联主/子', 'ko': '메인/서브 연동'})) + self._labelMainGap.setStringValue_(lx({'en': 'Main', 'ja': 'メイン', 'zh': '主', 'ko': '메인'})) + self._labelSubGap.setStringValue_(lx({'en': 'Sub', 'ja': 'サブ', 'zh': '子', 'ko': '서브'})) + self._gapSyncHV.setTitle_(lx({'en': 'Sync H/V', 'ja': '縦横同期', 'zh': '同步H/V', 'ko': 'H/V 동기화'})) self._resetButton.setTitle_(lx({'en': 'Reset', 'ja': 'リセット', 'zh': '重置', 'ko': '초기화'})) self._applyReadableColours() @@ -189,7 +243,10 @@ def _applyReadableColours(self): """Glyphs の外観下でラベル色が誤解釈されることがあるため、明示的にコントラストを確保する。""" label_colour = NSColor.labelColor() control_colour = NSColor.controlTextColor() - editable = {self._mainH, self._mainV, self._subH, self._subV} + editable = { + self._mainH, self._mainV, self._subH, self._subV, + self._mainGapH, self._mainGapV, self._subGapH, self._subGapV, + } def visit(view): if view is None: @@ -205,7 +262,8 @@ def visit(view): visit(self.panel.contentView()) for btn in (self._radioDivision, self._radioUnit, self._mainSync, self._subSync, self._snapCheck, - self._radioSquare, self._radioTriangle, self._radioHorizontal, self._radioVertical): + self._radioSquare, self._radioTriangle, self._radioHorizontal, self._radioVertical, + self._gapEnable, self._gapSyncMainSub, self._gapSyncHV): self._setButtonTitleColour_(btn, label_colour) for btn in (self._cancelButton, self._okButton): self._setButtonTitleColour_(btn, control_colour) @@ -247,6 +305,10 @@ def _loadFromDict(self, s): self._subHStep.setIntValue_(int(s.get('subDivX', 2))) self._subVStep.setIntValue_(int(s.get('subDivY', 2))) self._subSync.setState_(1 if s.get('subSync', False) else 0) + self._gapEnable.setState_(1 if s.get('gapEnabled', False) else 0) + self._gapSyncMainSub.setState_(1 if s.get('gapSyncMainSub', False) else 0) + self._gapSyncHV.setState_(1 if s.get('gapSyncHV', False) else 0) + self._setGapFieldValues(s) mc = s.get('mainColor', [0.2, 0.5, 1.0, 0.4]) sc = s.get('subColor', [0.2, 0.5, 1.0, 0.15]) @@ -274,8 +336,10 @@ def _loadFromDict(self, s): self._propagateMainHToV() if self._subSync.state() == 1: self._propagateSubHToV() + self._normaliseGapLinks() self._updateSyncFieldAvailability() self._updateSubGridCaptionForMode() + self._updateGapAvailability() @objc.python_method def _applyMainGridFieldsFromSettings(self, s): @@ -306,6 +370,30 @@ def _updateSubGridCaptionForMode(self): 'ko': '서브 그리드(메인 분할 수)', })) + @objc.python_method + def _formatUnitValue(self, value): + try: + f = float(value) + except (TypeError, ValueError): + f = 0.0 + if abs(f - int(f)) < 1e-6: + return str(int(f)) + return ('%.3f' % f).rstrip('0').rstrip('.') + + @objc.python_method + def _setGapFieldValues(self, s): + values = self.plugin._normalisedGapValues(s, include_disabled=True) + pairs = ( + (self._mainGapH, self._mainGapHStep, values[0]), + (self._mainGapV, self._mainGapVStep, values[1]), + (self._subGapH, self._subGapHStep, values[2]), + (self._subGapV, self._subGapVStep, values[3]), + ) + for field, stepper, value in pairs: + text = self._formatUnitValue(value) + field.setStringValue_(text) + stepper.setFloatValue_(float(value)) + @objc.python_method def _updateSyncFieldAvailability(self): main_on = self._mainSync.state() == 1 @@ -321,16 +409,68 @@ def _updateSyncFieldAvailability(self): if self._captionSubV is not None: self._captionSubV.setEnabled_(not sub_on) - def _saveFromUI(self): + @objc.python_method + def _updateGapAvailability(self): + is_tri = self._radioTriangle.state() == 1 + gap_on = self._gapEnable.state() == 1 + main_sub_on = self._gapSyncMainSub.state() == 1 + hv_on = self._gapSyncHV.state() == 1 + for ctl in (self._labelGridGap, self._gapEnable): + if ctl is not None: + ctl.setEnabled_(True) + for ctl in (self._gapSyncMainSub, self._labelMainGap, self._captionMainGapH, + self._mainGapH, self._mainGapHStep): + if ctl is not None: + ctl.setEnabled_(gap_on) + # Sync H/V and V fields: not applicable for triangle (V is unused) + for ctl in (self._gapSyncHV,): + if ctl is not None: + ctl.setEnabled_(gap_on and not is_tri) + for ctl in (self._captionMainGapV, self._mainGapV, self._mainGapVStep): + if ctl is not None: + ctl.setEnabled_(gap_on and not hv_on and not is_tri) + for ctl in (self._labelSubGap, self._captionSubGapH, self._subGapH, self._subGapHStep): + if ctl is not None: + ctl.setEnabled_(gap_on and not main_sub_on) + for ctl in (self._captionSubGapV, self._subGapV, self._subGapVStep): + if ctl is not None: + ctl.setEnabled_(gap_on and not main_sub_on and not hv_on and not is_tri) + + @objc.python_method + def _fieldInt(self, field, fallback=1, minimum=1): + try: + return max(minimum, int(float(field.stringValue() or str(fallback)))) + except (TypeError, ValueError): + return max(minimum, int(fallback)) + + @objc.python_method + def _fieldFloat(self, field, fallback=0.0, minimum=0.0): + try: + return max(minimum, float(field.stringValue() or str(fallback))) + except (TypeError, ValueError): + return max(minimum, float(fallback)) + + @objc.python_method + def _collectSettings(self): mode = 'division' if self._radioDivision.state() == 1 else 'unit' - hVal = max(1, int(float(self._mainH.stringValue() or '1'))) - vVal = max(1, int(float(self._mainV.stringValue() or '1'))) + hVal = self._fieldInt(self._mainH, 1, 1) + vVal = self._fieldInt(self._mainV, 1, 1) if self._mainSync.state() == 1: vVal = hVal - sx = max(1, int(float(self._subH.stringValue() or '1'))) - sy = max(1, int(float(self._subV.stringValue() or '1'))) + sx = self._fieldInt(self._subH, 1, 1) + sy = self._fieldInt(self._subV, 1, 1) if self._subSync.state() == 1: sy = sx + mainGapX = self._fieldFloat(self._mainGapH, 0.0, 0.0) + mainGapY = self._fieldFloat(self._mainGapV, 0.0, 0.0) + subGapX = self._fieldFloat(self._subGapH, 0.0, 0.0) + subGapY = self._fieldFloat(self._subGapV, 0.0, 0.0) + if self._gapSyncHV.state() == 1: + mainGapY = mainGapX + subGapY = subGapX + if self._gapSyncMainSub.state() == 1: + subGapX = mainGapX + subGapY = mainGapY mc_ns = self._mainColorWell.color().colorUsingColorSpaceName_('NSCalibratedRGBColorSpace') sc_ns = self._subColorWell.color().colorUsingColorSpaceName_('NSCalibratedRGBColorSpace') @@ -353,8 +493,18 @@ def _saveFromUI(self): 'snapEnabled': self._snapCheck.state() == 1, 'gridShape': 'triangle' if self._radioTriangle.state() == 1 else 'square', 'triOrientation': 'vertical' if self._radioVertical.state() == 1 else 'horizontal', + 'gapEnabled': self._gapEnable.state() == 1, + 'mainGapX': mainGapX, + 'mainGapY': mainGapY, + 'subGapX': subGapX, + 'subGapY': subGapY, + 'gapSyncHV': self._gapSyncHV.state() == 1, + 'gapSyncMainSub': self._gapSyncMainSub.state() == 1, } - self.plugin._saveSettings(Glyphs.font, s) + return s + + def _saveFromUI(self): + self.plugin._saveSettings(Glyphs.font, self._collectSettings()) def modeChanged_(self, sender): # NSButton radio は兄弟間で自動排他されないことがある。sender を基準に明示的に片方だけ ON にする。 @@ -379,6 +529,7 @@ def modeChanged_(self, sender): if self._subSync.state() == 1: self._propagateSubHToV() self._updateSyncFieldAvailability() + self._pushPreview() def syncToggled_(self, sender): if sender is self._mainSync and self._mainSync.state() == 1: @@ -386,6 +537,7 @@ def syncToggled_(self, sender): elif sender is self._subSync and self._subSync.state() == 1: self._propagateSubHToV() self._updateSyncFieldAvailability() + self._pushPreview() def shapeChanged_(self, sender): if sender is self._radioTriangle: @@ -395,6 +547,8 @@ def shapeChanged_(self, sender): self._radioSquare.setState_(1) self._radioTriangle.setState_(0) self._updateOrientationAvailability() + self._updateGapAvailability() + self._pushPreview() def orientationChanged_(self, sender): if sender is self._radioVertical: @@ -403,9 +557,22 @@ def orientationChanged_(self, sender): else: self._radioHorizontal.setState_(1) self._radioVertical.setState_(0) + self._pushPreview() def resetToDefaults_(self, sender): self._loadFromDict(self.plugin._defaultSettings()) + self._pushPreview() + + def previewChanged_(self, sender): + self._pushPreview() + + def colourChanged_(self, sender): + self._pushPreview() + + def gapChanged_(self, sender): + self._normaliseGapLinks() + self._updateGapAvailability() + self._pushPreview() @objc.python_method def _updateOrientationAvailability(self): @@ -424,33 +591,53 @@ def _propagateSubHToV(self): self._subV.setStringValue_(self._subH.stringValue()) self._syncStepperFromField(self._subV, self._subVStep) + @objc.python_method + def _normaliseGapLinks(self): + if self._gapSyncHV.state() == 1: + self._mainGapV.setStringValue_(self._mainGapH.stringValue()) + self._syncStepperFromField(self._mainGapV, self._mainGapVStep, 0) + self._subGapV.setStringValue_(self._subGapH.stringValue()) + self._syncStepperFromField(self._subGapV, self._subGapVStep, 0) + if self._gapSyncMainSub.state() == 1: + self._subGapH.setStringValue_(self._mainGapH.stringValue()) + self._subGapV.setStringValue_(self._mainGapV.stringValue()) + self._syncStepperFromField(self._subGapH, self._subGapHStep, 0) + self._syncStepperFromField(self._subGapV, self._subGapVStep, 0) + def stepperChanged_(self, sender): if self._isUpdatingControls: return field = None - for step, f in self._stepperPairs: + minimum = 1 + kind = None + for step, f, mn, k in self._stepperPairs: if sender is step: field = f + minimum = mn + kind = k break if field: self._isUpdatingControls = True try: - val = int(sender.intValue()) + val = max(minimum, int(sender.intValue())) field.setStringValue_(str(val)) # Sync paired field if needed - isMain = (sender is self._mainHStep or sender is self._mainVStep) - if isMain and self._mainSync.state() == 1: + if kind == 'mainGrid' and self._mainSync.state() == 1: self._mainH.setStringValue_(str(val)) self._mainV.setStringValue_(str(val)) self._mainHStep.setIntValue_(val) self._mainVStep.setIntValue_(val) - elif not isMain and self._subSync.state() == 1: + elif kind == 'subGrid' and self._subSync.state() == 1: self._subH.setStringValue_(str(val)) self._subV.setStringValue_(str(val)) self._subHStep.setIntValue_(val) self._subVStep.setIntValue_(val) finally: self._isUpdatingControls = False + if kind in ('mainGap', 'subGap'): + self._normaliseGapLinks() + self._updateGapAvailability() + self._pushPreview() def controlTextDidEndEditing_(self, notification): obj = notification.object() @@ -470,13 +657,34 @@ def controlTextDidEndEditing_(self, notification): self._syncStepperFromField(self._subV, self._subVStep) if self._subSync.state() == 1: self._propagateSubHToV() + elif obj is self._mainGapH: + self._syncStepperFromField(self._mainGapH, self._mainGapHStep, 0) + self._normaliseGapLinks() + elif obj is self._mainGapV: + self._syncStepperFromField(self._mainGapV, self._mainGapVStep, 0) + self._normaliseGapLinks() + elif obj is self._subGapH: + self._syncStepperFromField(self._subGapH, self._subGapHStep, 0) + self._normaliseGapLinks() + elif obj is self._subGapV: + self._syncStepperFromField(self._subGapV, self._subGapVStep, 0) + self._normaliseGapLinks() + self._pushPreview() + + def controlTextDidChange_(self, notification): + if self._isUpdatingControls: + return + obj = notification.object() + if obj in (self._mainGapH, self._mainGapV, self._subGapH, self._subGapV): + self._normaliseGapLinks() + self._pushPreview() @objc.python_method - def _syncStepperFromField(self, field, stepper): + def _syncStepperFromField(self, field, stepper, minimum=1): try: - val = max(1, min(999, int(float(field.stringValue() or '1')))) + val = max(minimum, min(999, int(float(field.stringValue() or str(minimum))))) except (TypeError, ValueError): - val = 1 + val = minimum self._isUpdatingControls = True try: field.setStringValue_(str(val)) @@ -484,14 +692,29 @@ def _syncStepperFromField(self, field, stepper): finally: self._isUpdatingControls = False + @objc.python_method + def _pushPreview(self): + try: + self.plugin._previewSettings = self._collectSettings() + Glyphs.redraw() + except Exception: + print(traceback.format_exc()) + def ok_(self, sender): self._saveFromUI() + self.plugin._clearPreview() self.panel.orderOut_(None) self.plugin._loadPrefs() Glyphs.redraw() def cancel_(self, sender): + self.plugin._clearPreview() self.panel.orderOut_(None) + Glyphs.redraw() + + def windowWillClose_(self, notification): + self.plugin._clearPreview() + Glyphs.redraw() # ───────────────────────────────────────────── @@ -512,6 +735,7 @@ def settings(self): @objc.python_method def start(self): self._loadPrefs() + self._previewSettings = None self._settingsController = SettingsPanelController.alloc().initWithPlugin_(self) showLabel = Glyphs.localize({ @@ -565,6 +789,18 @@ def _toggleGrid_(self, sender): def _showSettings_(self, sender): self._settingsController.show() + @objc.python_method + def _clearPreview(self): + self._previewSettings = None + + @objc.python_method + def _effectiveSettings(self, font): + if getattr(self, '_previewSettings', None) is not None: + s = self._getSettings(font) + s.update(self._previewSettings) + return s + return self._getSettings(font) + def _snapDuringDrag_(self, notification): if not self.gridVisible: return @@ -601,6 +837,7 @@ def _snapDuringDrag_(self, notification): angle_deg = self._effectiveItalicAngleDegrees(layer) tan_shear = math.tan(math.radians(angle_deg)) shape = s['gridShape'] + mainGapX, mainGapY, subGapX, subGapY = self._normalisedGapValues(s) for node in selectedNodes: pos = node.position @@ -613,7 +850,11 @@ def _snapDuringDrag_(self, notification): if shape == 'triangle': orient = s['triOrientation'] - if orient == 'horizontal': + _, _, subGapX, _ = self._normalisedGapValues(s) + if subGapX > 0.0: + families = self._triFamilies(orient, stepX, stepY, ySnapOrigin) + su, sv = self._snapTriangleWithGap(pu, pv, families, subGapX) + elif orient == 'horizontal': # Lattice: P(m,n) = (m*stepX + n*stepX/2, n*stepY + ySnapOrigin) n_snap = round((pv - ySnapOrigin) / stepY) m_snap = round((pu - n_snap * stepX * 0.5) / stepX) @@ -629,8 +870,8 @@ def _snapDuringDrag_(self, notification): snappedY = sv else: # Square grid: snap u and y independently - snappedY = round((pv - ySnapOrigin) / stepY) * stepY + ySnapOrigin - u_snapped = round(pu / stepX) * stepX + snappedY = self._nearestGridCoord(pv, ySnapOrigin, mainY, mainGapY, stepY, subGapY) + u_snapped = self._nearestGridCoord(pu, 0.0, mainX, mainGapX, stepX, subGapX) snappedX = u_snapped + tan_shear * (snappedY - pivot) dx = snappedX - pos.x @@ -731,6 +972,7 @@ def _handleArrowKeyEvent(self, event): stepX, stepY = self._subIntervals(s, mainX, mainY) if stepX <= 0 or stepY <= 0: stepX, stepY = mainX, mainY + # ギャップは線の中心を分離するだけなので、矢印移動量は従来の間隔を維持する。 dx, dy = 0.0, 0.0 if keyCode == 123: @@ -777,7 +1019,7 @@ def _drawGrid_(self, layer, info): scale = info.get('Scale', 1.0) if isinstance(info, dict) else 1.0 lineWidth = 1.0 / scale - s = self._getSettings(Glyphs.font) + s = self._effectiveSettings(Glyphs.font) mainX, mainY = self._mainIntervals(layer, s) subX, subY = self._subIntervals(s, mainX, mainY) grid_mode = s['mode'] @@ -787,30 +1029,42 @@ def _drawGrid_(self, layer, info): if shape == 'triangle': orient = s['triOrientation'] y_origin = 0.0 if grid_mode == 'unit' else yBottom + mainGapX, mainGapY, subGapX, subGapY = self._normalisedGapValues(s) if subX > 0 and subY > 0: - self._strokeTriGrid(width, yTop, yBottom, subX, subY, lineWidth, self._colorFromList(s['subColor']), orient, y_origin, layer, pivot_y) + self._strokeTriGrid(width, yTop, yBottom, subX, subY, lineWidth, self._colorFromList(s['subColor']), orient, y_origin, layer, pivot_y, subGapX) if mainX > 0 and mainY > 0: - self._strokeTriGrid(width, yTop, yBottom, mainX, mainY, lineWidth, self._colorFromList(s['mainColor']), orient, y_origin, layer, pivot_y) + self._strokeTriGrid(width, yTop, yBottom, mainX, mainY, lineWidth, self._colorFromList(s['mainColor']), orient, y_origin, layer, pivot_y, mainGapX) else: + mainGapX, mainGapY, subGapX, subGapY = self._normalisedGapValues(s) if subX > 0 and subY > 0: - self._strokeGrid(width, yTop, yBottom, subX, subY, lineWidth, self._colorFromList(s['subColor']), grid_mode, layer, pivot_y) + self._strokeGrid(width, yTop, yBottom, subX, subY, lineWidth, self._colorFromList(s['subColor']), grid_mode, layer, pivot_y, subGapX, subGapY) if mainX > 0 and mainY > 0: - self._strokeGrid(width, yTop, yBottom, mainX, mainY, lineWidth, self._colorFromList(s['mainColor']), grid_mode, layer, pivot_y) + self._strokeGrid(width, yTop, yBottom, mainX, mainY, lineWidth, self._colorFromList(s['mainColor']), grid_mode, layer, pivot_y, mainGapX, mainGapY) except Exception: print(traceback.format_exc()) @objc.python_method - def _strokeGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, grid_mode, layer, pivot_y): + def _strokeGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, grid_mode, layer, pivot_y, gapX=0.0, gapY=0.0): color.set() path = NSBezierPath.alloc().init() path.setLineWidth_(lineWidth) + gapX = max(0.0, float(gapX or 0.0)) + gapY = max(0.0, float(gapY or 0.0)) + + def offsetsForGap(gap): + if gap <= 0: + return (0.0,) + half = gap * 0.5 + return (-half, half) # Draw in glyph coordinate space (straight vertical / horizontal lines). # Vertical lines u = stepX while u < width: - path.moveToPoint_(NSPoint(u, yBottom)) - path.lineToPoint_(NSPoint(u, yTop)) + for offset in offsetsForGap(gapX): + x = u + offset + path.moveToPoint_(NSPoint(x, yBottom)) + path.lineToPoint_(NSPoint(x, yTop)) u += stepX # Horizontal lines @@ -819,8 +1073,10 @@ def _strokeGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, grid n = int(math.ceil((yBottom - y_origin) / stepY)) y = y_origin + n * stepY while y <= yTop: - path.moveToPoint_(NSPoint(0.0, y)) - path.lineToPoint_(NSPoint(width, y)) + for offset in offsetsForGap(gapY): + yy = y + offset + path.moveToPoint_(NSPoint(0.0, yy)) + path.lineToPoint_(NSPoint(width, yy)) y += stepY # Apply italic shear via Glyphs API (same pivot used by Glyphs itself). @@ -831,19 +1087,43 @@ def _strokeGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, grid path.stroke() @objc.python_method - def _strokeTriGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, orientation, y_origin, layer, pivot_y): + def _addGapLine(self, path, x0, y0, x1, y1, gap): + """Add one line (gap<=0) or two parallel lines offset ±gap/2 along the normal (gap>0).""" + if gap <= 0.0: + path.moveToPoint_(NSPoint(x0, y0)) + path.lineToPoint_(NSPoint(x1, y1)) + return + dx = x1 - x0 + dy = y1 - y0 + L = math.sqrt(dx * dx + dy * dy) + if L < 1e-12: + path.moveToPoint_(NSPoint(x0, y0)) + path.lineToPoint_(NSPoint(x1, y1)) + return + nx = -dy / L + ny = dx / L + half = gap * 0.5 + for sign in (-1.0, 1.0): + ox = sign * half * nx + oy = sign * half * ny + path.moveToPoint_(NSPoint(x0 + ox, y0 + oy)) + path.lineToPoint_(NSPoint(x1 + ox, y1 + oy)) + + @objc.python_method + def _strokeTriGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, orientation, y_origin, layer, pivot_y, gap=0.0): if orientation == 'vertical': - self._strokeTriGridV(width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y) + self._strokeTriGridV(width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap) else: - self._strokeTriGridH(width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y) + self._strokeTriGridH(width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap) @objc.python_method - def _strokeTriGridH(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y): + def _strokeTriGridH(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap=0.0): """Horizontal tri-grid: horizontal lines + ±diagonal lines (slope = 2*stepY/stepX).""" color.set() path = NSBezierPath.alloc().init() path.setLineWidth_(lineWidth) slope = 2.0 * stepY / stepX + gap = max(0.0, float(gap or 0.0)) # Horizontal lines, anchored at y_origin n_start = int(math.floor((yBottom - y_origin) / stepY)) @@ -851,8 +1131,7 @@ def _strokeTriGridH(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, for n in range(n_start, n_end + 1): y = y_origin + n * stepY if yBottom <= y <= yTop: - path.moveToPoint_(NSPoint(0.0, y)) - path.lineToPoint_(NSPoint(width, y)) + self._addGapLine(path, 0.0, y, width, y, gap) # Diagonal lines pass through (m*stepX, y_origin). # "/" : y - y_origin = slope*(x - m*stepX) → x = (y-y_origin)/slope + m*stepX @@ -866,14 +1145,12 @@ def _strokeTriGridH(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, x0 = (yBottom - y_origin) / slope + m * stepX x1 = (yTop - y_origin) / slope + m * stepX if not (x1 < 0 or x0 > width): - path.moveToPoint_(NSPoint(x0, yBottom)) - path.lineToPoint_(NSPoint(x1, yTop)) + self._addGapLine(path, x0, yBottom, x1, yTop, gap) # "\" x0b = m * stepX - (yBottom - y_origin) / slope x1b = m * stepX - (yTop - y_origin) / slope if not (x1b > width or x0b < 0): - path.moveToPoint_(NSPoint(x0b, yBottom)) - path.lineToPoint_(NSPoint(x1b, yTop)) + self._addGapLine(path, x0b, yBottom, x1b, yTop, gap) angle_deg = self._effectiveItalicAngleDegrees(layer) if abs(angle_deg) > 0.001: @@ -881,18 +1158,18 @@ def _strokeTriGridH(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, path.stroke() @objc.python_method - def _strokeTriGridV(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y): + def _strokeTriGridV(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap=0.0): """Vertical tri-grid: vertical lines + ±diagonal lines (slope = stepY/(2*stepX)).""" color.set() path = NSBezierPath.alloc().init() path.setLineWidth_(lineWidth) slope_v = stepY / (2.0 * stepX) + gap = max(0.0, float(gap or 0.0)) # Vertical lines u = stepX while u < width: - path.moveToPoint_(NSPoint(u, yBottom)) - path.lineToPoint_(NSPoint(u, yTop)) + self._addGapLine(path, u, yBottom, u, yTop, gap) u += stepX # Diagonal lines pass through (0, m*stepY + y_origin). @@ -909,21 +1186,144 @@ def _strokeTriGridV(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, y_xW = slope_v * width + base_y y_lo, y_hi = min(y_x0, y_xW), max(y_x0, y_xW) if not (y_hi < yBottom or y_lo > yTop): - path.moveToPoint_(NSPoint(0.0, y_x0)) - path.lineToPoint_(NSPoint(width, y_xW)) + self._addGapLine(path, 0.0, y_x0, width, y_xW, gap) # "\" y_x0b = base_y y_xWb = -slope_v * width + base_y y_lo2, y_hi2 = min(y_x0b, y_xWb), max(y_x0b, y_xWb) if not (y_hi2 < yBottom or y_lo2 > yTop): - path.moveToPoint_(NSPoint(0.0, y_x0b)) - path.lineToPoint_(NSPoint(width, y_xWb)) + self._addGapLine(path, 0.0, y_x0b, width, y_xWb, gap) angle_deg = self._effectiveItalicAngleDegrees(layer) if abs(angle_deg) > 0.001: path.transformWithAngle_center_(angle_deg, pivot_y) path.stroke() + @objc.python_method + def _floatSetting(self, s, key, default=0.0): + try: + return float(s.get(key, default)) + except (TypeError, ValueError): + return float(default) + + @objc.python_method + def _normalisedGapValues(self, s, include_disabled=False): + mainGapX = max(0.0, self._floatSetting(s, 'mainGapX', 0.0)) + mainGapY = max(0.0, self._floatSetting(s, 'mainGapY', 0.0)) + subGapX = max(0.0, self._floatSetting(s, 'subGapX', 0.0)) + subGapY = max(0.0, self._floatSetting(s, 'subGapY', 0.0)) + if s.get('gapSyncHV', False): + mainGapY = mainGapX + subGapY = subGapX + if s.get('gapSyncMainSub', False): + subGapX = mainGapX + subGapY = mainGapY + if include_disabled: + return mainGapX, mainGapY, subGapX, subGapY + if not s.get('gapEnabled', False): + return 0.0, 0.0, 0.0, 0.0 + return mainGapX, mainGapY, subGapX, subGapY + + @objc.python_method + def _gapOffsets(self, gap): + gap = max(0.0, float(gap or 0.0)) + if gap <= 0.0: + return (0.0,) + half = gap * 0.5 + return (-half, half) + + @objc.python_method + def _nearestGridCoord(self, value, origin, mainStep, mainGap, subStep, subGap): + best = None + for step, gap in ((mainStep, mainGap), (subStep, subGap)): + if step <= 0: + continue + for offset in self._gapOffsets(gap): + k = round((value - origin - offset) / step) + candidate = origin + k * step + offset + distance = abs(candidate - value) + if best is None or distance < best[0]: + best = (distance, candidate) + return best[1] if best is not None else value + + @objc.python_method + def _triFamilies(self, orient, stepX, stepY, O_y): + """Return 3 line families as (ux, uy, spacing, phase) for a tri-grid. + + Each family is defined by its unit normal (ux, uy) and the series + t = phase + k * spacing (k integer) + where t = ux*x + uy*y is the projection of a point onto the normal. + Gap-offset lines lie at t = phase + k*spacing ± gap/2. + """ + if orient == 'horizontal': + slope = 2.0 * stepY / stepX # slope of diagonal lines + sq = math.sqrt(1.0 + slope * slope) + return [ + # horizontal lines: normal (0,1), phase = O_y, spacing = stepY + (0.0, 1.0, stepY, O_y), + # "/" lines: using normal (slope, -1)/sq so t increases with m + (slope / sq, -1.0 / sq, slope * stepX / sq, -O_y / sq), + # "\" lines: normal (slope, 1)/sq, phase = O_y/sq + (slope / sq, 1.0 / sq, slope * stepX / sq, O_y / sq), + ] + else: # vertical + slope_v = stepY / (2.0 * stepX) # slope of diagonal lines + sq_v = math.sqrt(1.0 + slope_v * slope_v) + return [ + # vertical lines: normal (1,0), phase = 0, spacing = stepX + (1.0, 0.0, stepX, 0.0), + # "/" lines: normal (-slope_v, 1)/sq_v, phase = O_y/sq_v + (-slope_v / sq_v, 1.0 / sq_v, stepY / sq_v, O_y / sq_v), + # "\" lines: normal (slope_v, 1)/sq_v, phase = O_y/sq_v + ( slope_v / sq_v, 1.0 / sq_v, stepY / sq_v, O_y / sq_v), + ] + + @objc.python_method + def _nearestLineValue(self, t, phase, spacing, gap): + """Return the value (from the series phase+k*spacing ± gap/2) nearest to t.""" + if spacing <= 0: + return t + k0 = round((t - phase) / spacing) + best = None + for dk in (-1, 0, 1): + base = phase + (k0 + dk) * spacing + offsets = (-gap * 0.5, gap * 0.5) if gap > 0.0 else (0.0,) + for off in offsets: + val = base + off + d = abs(val - t) + if best is None or d < best[0]: + best = (d, val) + return best[1] if best is not None else t + + @objc.python_method + def _snapTriangleWithGap(self, pu, pv, families, gap): + """Snap (pu,pv) to the nearest intersection of gap-offset triangle lines. + + For each of the 3 families finds the nearest line (including ±gap/2 offset), + then tries all 3 pairs of families and returns the intersection closest to P. + """ + ws = [] + for ux, uy, spacing, phase in families: + t = ux * pu + uy * pv + ws.append(self._nearestLineValue(t, phase, spacing, gap)) + + best = None + for i, j in ((0, 1), (0, 2), (1, 2)): + uxi, uyi = families[i][0], families[i][1] + uxj, uyj = families[j][0], families[j][1] + det = uxi * uyj - uyi * uxj + if abs(det) < 1e-12: + continue + x = (ws[i] * uyj - ws[j] * uyi) / det + y = (uxi * ws[j] - uxj * ws[i]) / det + d2 = (x - pu) ** 2 + (y - pv) ** 2 + if best is None or d2 < best[0]: + best = (d2, x, y) + + if best is None: + return pu, pv + return best[1], best[2] + @objc.python_method def _ySnapOriginForLayer(self, layer, s): """Y snap / horizontal grid phase: baseline (0) in Unit mode, descender in Division mode.""" @@ -1076,6 +1476,13 @@ def _defaultSettings(self): 'snapEnabled': True, 'gridShape': 'square', 'triOrientation': 'horizontal', + 'gapEnabled': False, + 'mainGapX': 0.0, + 'mainGapY': 0.0, + 'subGapX': 0.0, + 'subGapY': 0.0, + 'gapSyncHV': False, + 'gapSyncMainSub': False, } @objc.python_method @@ -1106,6 +1513,13 @@ def _getSettings(self, font): 'snapEnabled': bool(d.get(p + '.snapEnabled', True)), 'gridShape': d.get(p + '.gridShape', 'square'), 'triOrientation': d.get(p + '.triOrientation', 'horizontal'), + 'gapEnabled': bool(d.get(p + '.gapEnabled', False)), + 'mainGapX': float(d.get(p + '.mainGapX', 0.0)), + 'mainGapY': float(d.get(p + '.mainGapY', 0.0)), + 'subGapX': float(d.get(p + '.subGapX', 0.0)), + 'subGapY': float(d.get(p + '.subGapY', 0.0)), + 'gapSyncHV': bool(d.get(p + '.gapSyncHV', False)), + 'gapSyncMainSub': bool(d.get(p + '.gapSyncMainSub', False)), } @objc.python_method @@ -1127,6 +1541,13 @@ def _saveSettings(self, font, s): d[p + '.snapEnabled'] = s['snapEnabled'] d[p + '.gridShape'] = s['gridShape'] d[p + '.triOrientation'] = s['triOrientation'] + d[p + '.gapEnabled'] = s.get('gapEnabled', False) + d[p + '.mainGapX'] = s.get('mainGapX', 0.0) + d[p + '.mainGapY'] = s.get('mainGapY', 0.0) + d[p + '.subGapX'] = s.get('subGapX', 0.0) + d[p + '.subGapY'] = s.get('subGapY', 0.0) + d[p + '.gapSyncHV'] = s.get('gapSyncHV', False) + d[p + '.gapSyncMainSub'] = s.get('gapSyncMainSub', False) @objc.python_method def _loadPrefs(self): From 8a534bb60b9e56aab6913d65a0e261b72f9c8a7e Mon Sep 17 00:00:00 2001 From: Palf Date: Sun, 28 Jun 2026 19:15:49 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=E3=82=BA=E3=83=BC=E3=83=A0=E3=81=AB?= =?UTF-8?q?=E9=96=A2=E4=BF=82=E3=81=AA=E3=81=8F=E3=82=B0=E3=83=AA=E3=83=83?= =?UTF-8?q?=E3=83=89=E7=B7=9A=E5=B9=85=E3=82=921px=E3=81=AB=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit info['Scale'] の取得を try/except に変更し、PyObjC の NSDictionary が isinstance(info, dict) を通過しないケースでも正しいスケール係数を取得する。 これによりズームレベルに関わらず常に1ピクセル幅で描画される。 Co-authored-by: Cursor --- SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py index 3b49814..0ba8e0d 100644 --- a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py +++ b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py @@ -1016,8 +1016,11 @@ def _drawGrid_(self, layer, info): master = layer.associatedFontMaster() yTop = master.ascender if master else 800.0 yBottom = master.descender if master else -200.0 - scale = info.get('Scale', 1.0) if isinstance(info, dict) else 1.0 - lineWidth = 1.0 / scale + try: + scale = float(info['Scale']) + except Exception: + scale = 1.0 + lineWidth = 1.0 / max(scale, 0.01) s = self._effectiveSettings(Glyphs.font) mainX, mainY = self._mainIntervals(layer, s) From 1a76b2a052b95529d60a727e6def8975561499c1 Mon Sep 17 00:00:00 2001 From: Palf Date: Sun, 28 Jun 2026 19:43:52 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=E3=82=B0=E3=83=AA=E3=83=83?= =?UTF-8?q?=E3=83=89=E3=82=AA=E3=83=95=E3=82=BB=E3=83=83=E3=83=88=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 設定に offsetX / offsetY / offsetSyncHV を追加(_defaultSettings/_getSettings/_saveSettings) - _strokeGrid に offsetX/offsetY 引数を追加: 垂直線は offsetX+k*stepX、水平線は y_origin+offsetY を位相起点として使用 - _strokeTriGridH/V に offsetX/offsetY を追加: すべての線族の位相起点を (offsetX, y_origin+offsetY) にシフト - _triFamilies の位相計算をオフセット対応に更新(水平・垂直三角グリッド両方) - _snapDuringDrag_ でスクエア・三角グリッド双方のスナップ計算にオフセットを反映 - 設定パネルに「Grid Offset:」セクション(H/V 独立、Sync H/V リンク)を追加 - xib: ウィンドウ高さ 490→520、既存コントロールを +30 上シフト、オフセット行を挿入 - nib 再コンパイル済み Co-authored-by: Cursor --- .../Resources/SnappingGridSettings.nib | Bin 29220 -> 31980 bytes .../Resources/SnappingGridSettings.xib | 161 ++++++--- .../Contents/Resources/plugin.py | 315 ++++++++++++------ 3 files changed, 321 insertions(+), 155 deletions(-) diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.nib b/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.nib index f43d85383e41ffe2c8f988217a235fa73811784a..a851582055a920ba582a722c72660f79dab12e57 100644 GIT binary patch delta 7089 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 diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.xib b/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.xib index da0699e..cbd5909 100644 --- a/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.xib +++ b/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.xib @@ -58,26 +58,34 @@ + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + @@ -173,19 +181,19 @@ - + - + - + @@ -193,35 +201,35 @@ - + - + - + - + - + @@ -229,19 +237,19 @@ - + - + - + @@ -249,114 +257,169 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py index 0ba8e0d..45ff217 100644 --- a/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py +++ b/SnappingGrid.glyphsPlugin/Contents/Resources/plugin.py @@ -69,6 +69,14 @@ class SettingsPanelController(NSObject): _subGapV = objc.IBOutlet() _subGapHStep = objc.IBOutlet() _subGapVStep = objc.IBOutlet() + _labelGridOffset = objc.IBOutlet() + _captionOffsetH = objc.IBOutlet() + _captionOffsetV = objc.IBOutlet() + _offsetH = objc.IBOutlet() + _offsetV = objc.IBOutlet() + _offsetHStep = objc.IBOutlet() + _offsetVStep = objc.IBOutlet() + _offsetSyncHV = objc.IBOutlet() def initWithPlugin_(self, plugin): self = objc.super(SettingsPanelController, self).init() @@ -125,8 +133,16 @@ def _wireControls(self): step.setValueWraps_(False) step.setTarget_(self) step.setAction_(step_action) + for step in (self._offsetHStep, self._offsetVStep): + step.setMinValue_(-9999) + step.setMaxValue_(9999) + step.setIncrement_(1) + step.setValueWraps_(False) + step.setTarget_(self) + step.setAction_(step_action) for field in (self._mainH, self._mainV, self._subH, self._subV, - self._mainGapH, self._mainGapV, self._subGapH, self._subGapV): + self._mainGapH, self._mainGapV, self._subGapH, self._subGapV, + self._offsetH, self._offsetV): field.setEditable_(True) field.setSelectable_(True) field.setBezeled_(True) @@ -141,6 +157,8 @@ def _wireControls(self): (self._mainGapVStep, self._mainGapV, 0, 'mainGap'), (self._subGapHStep, self._subGapH, 0, 'subGap'), (self._subGapVStep, self._subGapV, 0, 'subGap'), + (self._offsetHStep, self._offsetH, -9999, 'offset'), + (self._offsetVStep, self._offsetV, -9999, 'offset'), ) mode_sel = NSSelectorFromString('modeChanged:') self._radioDivision.setTarget_(self) @@ -169,6 +187,10 @@ def _wireControls(self): for button in (self._gapEnable, self._gapSyncMainSub, self._gapSyncHV): button.setTarget_(self) button.setAction_(gap_sel) + offset_sel = NSSelectorFromString('offsetChanged:') + if self._offsetSyncHV is not None: + self._offsetSyncHV.setTarget_(self) + self._offsetSyncHV.setAction_(offset_sel) preview_sel = NSSelectorFromString('previewChanged:') self._snapCheck.setTarget_(self) self._snapCheck.setAction_(preview_sel) @@ -246,6 +268,7 @@ def _applyReadableColours(self): editable = { self._mainH, self._mainV, self._subH, self._subV, self._mainGapH, self._mainGapV, self._subGapH, self._subGapV, + self._offsetH, self._offsetV, } def visit(view): @@ -263,7 +286,7 @@ def visit(view): for btn in (self._radioDivision, self._radioUnit, self._mainSync, self._subSync, self._snapCheck, self._radioSquare, self._radioTriangle, self._radioHorizontal, self._radioVertical, - self._gapEnable, self._gapSyncMainSub, self._gapSyncHV): + self._gapEnable, self._gapSyncMainSub, self._gapSyncHV, self._offsetSyncHV): self._setButtonTitleColour_(btn, label_colour) for btn in (self._cancelButton, self._okButton): self._setButtonTitleColour_(btn, control_colour) @@ -336,7 +359,19 @@ def _loadFromDict(self, s): self._propagateMainHToV() if self._subSync.state() == 1: self._propagateSubHToV() + offsetX = s.get('offsetX', 0.0) + offsetY = s.get('offsetY', 0.0) + if self._offsetH is not None: + self._offsetH.setStringValue_(self._formatUnitValue(offsetX)) + self._syncStepperFromField(self._offsetH, self._offsetHStep, -9999) + if self._offsetV is not None: + self._offsetV.setStringValue_(self._formatUnitValue(offsetY)) + self._syncStepperFromField(self._offsetV, self._offsetVStep, -9999) + if self._offsetSyncHV is not None: + self._offsetSyncHV.setState_(1 if s.get('offsetSyncHV', False) else 0) + self._normaliseGapLinks() + self._normaliseOffsetLink() self._updateSyncFieldAvailability() self._updateSubGridCaptionForMode() self._updateGapAvailability() @@ -472,6 +507,11 @@ def _collectSettings(self): subGapX = mainGapX subGapY = mainGapY + offsetX = self._fieldFloat(self._offsetH, 0.0, -9999.0) + offsetY = self._fieldFloat(self._offsetV, 0.0, -9999.0) + if self._offsetSyncHV is not None and self._offsetSyncHV.state() == 1: + offsetY = offsetX + mc_ns = self._mainColorWell.color().colorUsingColorSpaceName_('NSCalibratedRGBColorSpace') sc_ns = self._subColorWell.color().colorUsingColorSpaceName_('NSCalibratedRGBColorSpace') mc = [mc_ns.redComponent(), mc_ns.greenComponent(), mc_ns.blueComponent(), mc_ns.alphaComponent()] if mc_ns else [0.2, 0.5, 1.0, 0.4] @@ -500,6 +540,9 @@ def _collectSettings(self): 'subGapY': subGapY, 'gapSyncHV': self._gapSyncHV.state() == 1, 'gapSyncMainSub': self._gapSyncMainSub.state() == 1, + 'offsetX': offsetX, + 'offsetY': offsetY, + 'offsetSyncHV': self._offsetSyncHV.state() == 1 if self._offsetSyncHV is not None else False, } return s @@ -574,6 +617,19 @@ def gapChanged_(self, sender): self._updateGapAvailability() self._pushPreview() + def offsetChanged_(self, sender): + self._normaliseOffsetLink() + self._pushPreview() + + @objc.python_method + def _normaliseOffsetLink(self): + if self._offsetSyncHV is None or self._offsetSyncHV.state() != 1: + return + if self._offsetH is None or self._offsetV is None: + return + self._offsetV.setStringValue_(self._offsetH.stringValue()) + self._syncStepperFromField(self._offsetV, self._offsetVStep, -9999) + @objc.python_method def _updateOrientationAvailability(self): is_tri = self._radioTriangle.state() == 1 @@ -619,24 +675,30 @@ def stepperChanged_(self, sender): if field: self._isUpdatingControls = True try: - val = max(minimum, int(sender.intValue())) - field.setStringValue_(str(val)) - # Sync paired field if needed - if kind == 'mainGrid' and self._mainSync.state() == 1: - self._mainH.setStringValue_(str(val)) - self._mainV.setStringValue_(str(val)) - self._mainHStep.setIntValue_(val) - self._mainVStep.setIntValue_(val) - elif kind == 'subGrid' and self._subSync.state() == 1: - self._subH.setStringValue_(str(val)) - self._subV.setStringValue_(str(val)) - self._subHStep.setIntValue_(val) - self._subVStep.setIntValue_(val) + if kind == 'offset': + val_f = float(sender.doubleValue()) + field.setStringValue_(self._formatUnitValue(val_f)) + else: + val = max(minimum, int(sender.intValue())) + field.setStringValue_(str(val)) + # Sync paired field if needed + if kind == 'mainGrid' and self._mainSync.state() == 1: + self._mainH.setStringValue_(str(val)) + self._mainV.setStringValue_(str(val)) + self._mainHStep.setIntValue_(val) + self._mainVStep.setIntValue_(val) + elif kind == 'subGrid' and self._subSync.state() == 1: + self._subH.setStringValue_(str(val)) + self._subV.setStringValue_(str(val)) + self._subHStep.setIntValue_(val) + self._subVStep.setIntValue_(val) finally: self._isUpdatingControls = False if kind in ('mainGap', 'subGap'): self._normaliseGapLinks() self._updateGapAvailability() + elif kind == 'offset': + self._normaliseOffsetLink() self._pushPreview() def controlTextDidEndEditing_(self, notification): @@ -669,6 +731,11 @@ def controlTextDidEndEditing_(self, notification): elif obj is self._subGapV: self._syncStepperFromField(self._subGapV, self._subGapVStep, 0) self._normaliseGapLinks() + elif obj is self._offsetH: + self._syncStepperFromField(self._offsetH, self._offsetHStep, -9999) + self._normaliseOffsetLink() + elif obj is self._offsetV: + self._syncStepperFromField(self._offsetV, self._offsetVStep, -9999) self._pushPreview() def controlTextDidChange_(self, notification): @@ -677,17 +744,20 @@ def controlTextDidChange_(self, notification): obj = notification.object() if obj in (self._mainGapH, self._mainGapV, self._subGapH, self._subGapV): self._normaliseGapLinks() + elif obj in (self._offsetH, self._offsetV): + self._normaliseOffsetLink() self._pushPreview() @objc.python_method def _syncStepperFromField(self, field, stepper, minimum=1): + max_val = 9999 if minimum < 0 else 999 try: - val = max(minimum, min(999, int(float(field.stringValue() or str(minimum))))) + raw = float(field.stringValue() or str(minimum)) + val = max(minimum, min(max_val, int(raw))) except (TypeError, ValueError): - val = minimum + val = max(minimum, 0) self._isUpdatingControls = True try: - field.setStringValue_(str(val)) stepper.setIntValue_(val) finally: self._isUpdatingControls = False @@ -838,6 +908,8 @@ def _snapDuringDrag_(self, notification): tan_shear = math.tan(math.radians(angle_deg)) shape = s['gridShape'] mainGapX, mainGapY, subGapX, subGapY = self._normalisedGapValues(s) + offsetX = self._floatSetting(s, 'offsetX', 0.0) + offsetY = self._floatSetting(s, 'offsetY', 0.0) for node in selectedNodes: pos = node.position @@ -850,28 +922,30 @@ def _snapDuringDrag_(self, notification): if shape == 'triangle': orient = s['triOrientation'] - _, _, subGapX, _ = self._normalisedGapValues(s) - if subGapX > 0.0: - families = self._triFamilies(orient, stepX, stepY, ySnapOrigin) - su, sv = self._snapTriangleWithGap(pu, pv, families, subGapX) + _, _, subGapX_tri, _ = self._normalisedGapValues(s) + if subGapX_tri > 0.0: + families = self._triFamilies(orient, stepX, stepY, ySnapOrigin, offsetX, offsetY) + su, sv = self._snapTriangleWithGap(pu, pv, families, subGapX_tri) elif orient == 'horizontal': - # Lattice: P(m,n) = (m*stepX + n*stepX/2, n*stepY + ySnapOrigin) - n_snap = round((pv - ySnapOrigin) / stepY) - m_snap = round((pu - n_snap * stepX * 0.5) / stepX) - su = m_snap * stepX + n_snap * stepX * 0.5 - sv = n_snap * stepY + ySnapOrigin + # Lattice with offset: P(m,n) = (m*stepX + n*stepX/2 + offsetX, n*stepY + ySnapOrigin + offsetY) + O_y = ySnapOrigin + offsetY + n_snap = round((pv - O_y) / stepY) + m_snap = round((pu - offsetX - n_snap * stepX * 0.5) / stepX) + su = m_snap * stepX + n_snap * stepX * 0.5 + offsetX + sv = n_snap * stepY + O_y else: - # Lattice: P(m,n) = (n*stepX, m*stepY + n*stepY/2) - n_snap = round(pu / stepX) - m_snap = round((pv - n_snap * stepY * 0.5) / stepY) - su = n_snap * stepX - sv = m_snap * stepY + n_snap * stepY * 0.5 + # Lattice with offset: P(m,n) = (n*stepX + offsetX, m*stepY + n*stepY/2 + ySnapOrigin + offsetY) + O_y = ySnapOrigin + offsetY + n_snap = round((pu - offsetX) / stepX) + m_snap = round((pv - O_y - n_snap * stepY * 0.5) / stepY) + su = n_snap * stepX + offsetX + sv = m_snap * stepY + n_snap * stepY * 0.5 + O_y snappedX = su + tan_shear * (sv - pivot) snappedY = sv else: - # Square grid: snap u and y independently - snappedY = self._nearestGridCoord(pv, ySnapOrigin, mainY, mainGapY, stepY, subGapY) - u_snapped = self._nearestGridCoord(pu, 0.0, mainX, mainGapX, stepX, subGapX) + # Square grid: snap u and v independently using offset as phase origin + snappedY = self._nearestGridCoord(pv, ySnapOrigin + offsetY, mainY, mainGapY, stepY, subGapY) + u_snapped = self._nearestGridCoord(pu, offsetX, mainX, mainGapX, stepX, subGapX) snappedX = u_snapped + tan_shear * (snappedY - pivot) dx = snappedX - pos.x @@ -1029,30 +1103,34 @@ def _drawGrid_(self, layer, info): pivot_y = self._shearPivotY(layer) shape = s['gridShape'] + offsetX = self._floatSetting(s, 'offsetX', 0.0) + offsetY = self._floatSetting(s, 'offsetY', 0.0) if shape == 'triangle': orient = s['triOrientation'] y_origin = 0.0 if grid_mode == 'unit' else yBottom mainGapX, mainGapY, subGapX, subGapY = self._normalisedGapValues(s) if subX > 0 and subY > 0: - self._strokeTriGrid(width, yTop, yBottom, subX, subY, lineWidth, self._colorFromList(s['subColor']), orient, y_origin, layer, pivot_y, subGapX) + self._strokeTriGrid(width, yTop, yBottom, subX, subY, lineWidth, self._colorFromList(s['subColor']), orient, y_origin, layer, pivot_y, subGapX, offsetX, offsetY) if mainX > 0 and mainY > 0: - self._strokeTriGrid(width, yTop, yBottom, mainX, mainY, lineWidth, self._colorFromList(s['mainColor']), orient, y_origin, layer, pivot_y, mainGapX) + self._strokeTriGrid(width, yTop, yBottom, mainX, mainY, lineWidth, self._colorFromList(s['mainColor']), orient, y_origin, layer, pivot_y, mainGapX, offsetX, offsetY) else: mainGapX, mainGapY, subGapX, subGapY = self._normalisedGapValues(s) if subX > 0 and subY > 0: - self._strokeGrid(width, yTop, yBottom, subX, subY, lineWidth, self._colorFromList(s['subColor']), grid_mode, layer, pivot_y, subGapX, subGapY) + self._strokeGrid(width, yTop, yBottom, subX, subY, lineWidth, self._colorFromList(s['subColor']), grid_mode, layer, pivot_y, subGapX, subGapY, offsetX, offsetY) if mainX > 0 and mainY > 0: - self._strokeGrid(width, yTop, yBottom, mainX, mainY, lineWidth, self._colorFromList(s['mainColor']), grid_mode, layer, pivot_y, mainGapX, mainGapY) + self._strokeGrid(width, yTop, yBottom, mainX, mainY, lineWidth, self._colorFromList(s['mainColor']), grid_mode, layer, pivot_y, mainGapX, mainGapY, offsetX, offsetY) except Exception: print(traceback.format_exc()) @objc.python_method - def _strokeGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, grid_mode, layer, pivot_y, gapX=0.0, gapY=0.0): + def _strokeGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, grid_mode, layer, pivot_y, gapX=0.0, gapY=0.0, offsetX=0.0, offsetY=0.0): color.set() path = NSBezierPath.alloc().init() path.setLineWidth_(lineWidth) gapX = max(0.0, float(gapX or 0.0)) gapY = max(0.0, float(gapY or 0.0)) + offsetX = float(offsetX or 0.0) + offsetY = float(offsetY or 0.0) def offsetsForGap(gap): if gap <= 0: @@ -1060,24 +1138,29 @@ def offsetsForGap(gap): half = gap * 0.5 return (-half, half) - # Draw in glyph coordinate space (straight vertical / horizontal lines). - # Vertical lines - u = stepX - while u < width: - for offset in offsetsForGap(gapX): - x = u + offset - path.moveToPoint_(NSPoint(x, yBottom)) - path.lineToPoint_(NSPoint(x, yTop)) - u += stepX - - # Horizontal lines + # Vertical lines at offsetX + k*stepX, strictly inside (0, width) + if stepX > 0: + k = int(math.floor(-offsetX / stepX)) + 1 + u = offsetX + k * stepX + while u <= 0.0: + k += 1 + u = offsetX + k * stepX + while u < width: + for off in offsetsForGap(gapX): + x = u + off + path.moveToPoint_(NSPoint(x, yBottom)) + path.lineToPoint_(NSPoint(x, yTop)) + k += 1 + u = offsetX + k * stepX + + # Horizontal lines at y_base + n*stepY if stepY > 0: - y_origin = 0.0 if grid_mode == 'unit' else yBottom - n = int(math.ceil((yBottom - y_origin) / stepY)) - y = y_origin + n * stepY + y_base = (0.0 if grid_mode == 'unit' else yBottom) + offsetY + n = int(math.ceil((yBottom - y_base) / stepY)) + y = y_base + n * stepY while y <= yTop: - for offset in offsetsForGap(gapY): - yy = y + offset + for off in offsetsForGap(gapY): + yy = y + off path.moveToPoint_(NSPoint(0.0, yy)) path.lineToPoint_(NSPoint(width, yy)) y += stepY @@ -1113,45 +1196,47 @@ def _addGapLine(self, path, x0, y0, x1, y1, gap): path.lineToPoint_(NSPoint(x1 + ox, y1 + oy)) @objc.python_method - def _strokeTriGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, orientation, y_origin, layer, pivot_y, gap=0.0): + def _strokeTriGrid(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, orientation, y_origin, layer, pivot_y, gap=0.0, offsetX=0.0, offsetY=0.0): if orientation == 'vertical': - self._strokeTriGridV(width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap) + self._strokeTriGridV(width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap, offsetX, offsetY) else: - self._strokeTriGridH(width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap) + self._strokeTriGridH(width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap, offsetX, offsetY) @objc.python_method - def _strokeTriGridH(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap=0.0): + def _strokeTriGridH(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap=0.0, offsetX=0.0, offsetY=0.0): """Horizontal tri-grid: horizontal lines + ±diagonal lines (slope = 2*stepY/stepX).""" color.set() path = NSBezierPath.alloc().init() path.setLineWidth_(lineWidth) slope = 2.0 * stepY / stepX gap = max(0.0, float(gap or 0.0)) + O_y = y_origin + offsetY - # Horizontal lines, anchored at y_origin - n_start = int(math.floor((yBottom - y_origin) / stepY)) - n_end = int(math.ceil((yTop - y_origin) / stepY)) + # Horizontal lines anchored at O_y + n_start = int(math.floor((yBottom - O_y) / stepY)) + n_end = int(math.ceil((yTop - O_y) / stepY)) for n in range(n_start, n_end + 1): - y = y_origin + n * stepY + y = O_y + n * stepY if yBottom <= y <= yTop: self._addGapLine(path, 0.0, y, width, y, gap) - # Diagonal lines pass through (m*stepX, y_origin). - # "/" : y - y_origin = slope*(x - m*stepX) → x = (y-y_origin)/slope + m*stepX - # "\" : y - y_origin = -slope*(x - m*stepX) → x = m*stepX - (y-y_origin)/slope - extra = int(math.ceil((abs(yTop - y_origin) + abs(yBottom - y_origin)) / slope / stepX)) + 2 - m_min = -extra - m_max = int(math.ceil(width / stepX)) + extra + # Diagonal lines pass through (m*stepX + offsetX, O_y). + # "/" : y - O_y = slope*(x - (m*stepX + offsetX)) → x = (y-O_y)/slope + m*stepX + offsetX + # "\" : y - O_y = -slope*(x - (m*stepX + offsetX)) → x = m*stepX + offsetX - (y-O_y)/slope + extra = int(math.ceil((abs(yTop - O_y) + abs(yBottom - O_y)) / slope / stepX)) + 2 + m_min = int(math.ceil(-offsetX / stepX)) - extra - 1 + m_max = int(math.ceil((width - offsetX) / stepX)) + extra for m in range(m_min, m_max + 1): + ox = m * stepX + offsetX # "/" - x0 = (yBottom - y_origin) / slope + m * stepX - x1 = (yTop - y_origin) / slope + m * stepX + x0 = (yBottom - O_y) / slope + ox + x1 = (yTop - O_y) / slope + ox if not (x1 < 0 or x0 > width): self._addGapLine(path, x0, yBottom, x1, yTop, gap) # "\" - x0b = m * stepX - (yBottom - y_origin) / slope - x1b = m * stepX - (yTop - y_origin) / slope + x0b = ox - (yBottom - O_y) / slope + x1b = ox - (yTop - O_y) / slope if not (x1b > width or x0b < 0): self._addGapLine(path, x0b, yBottom, x1b, yTop, gap) @@ -1161,38 +1246,45 @@ def _strokeTriGridH(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, path.stroke() @objc.python_method - def _strokeTriGridV(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap=0.0): + def _strokeTriGridV(self, width, yTop, yBottom, stepX, stepY, lineWidth, color, y_origin, layer, pivot_y, gap=0.0, offsetX=0.0, offsetY=0.0): """Vertical tri-grid: vertical lines + ±diagonal lines (slope = stepY/(2*stepX)).""" color.set() path = NSBezierPath.alloc().init() path.setLineWidth_(lineWidth) slope_v = stepY / (2.0 * stepX) gap = max(0.0, float(gap or 0.0)) - - # Vertical lines - u = stepX - while u < width: - self._addGapLine(path, u, yBottom, u, yTop, gap) - u += stepX - - # Diagonal lines pass through (0, m*stepY + y_origin). - # "/" : y = slope_v*x + m*stepY + y_origin - # "\" : y = -slope_v*x + m*stepY + y_origin - extra = int(math.ceil((abs(yTop) + abs(yBottom)) / stepY + width * slope_v / stepY)) + 2 - m_min = int(math.floor((yBottom - y_origin) / stepY)) - extra - m_max = int(math.ceil((yTop - y_origin) / stepY)) + extra + O_y = y_origin + offsetY + + # Vertical lines at offsetX + k*stepX, strictly inside (0, width) + if stepX > 0: + k = int(math.floor(-offsetX / stepX)) + 1 + u = offsetX + k * stepX + while u <= 0.0: + k += 1 + u = offsetX + k * stepX + while u < width: + self._addGapLine(path, u, yBottom, u, yTop, gap) + k += 1 + u = offsetX + k * stepX + + # Diagonal lines pass through (offsetX, m*stepY + O_y). + # "/" : y = slope_v*(x - offsetX) + m*stepY + O_y + # "\" : y = -slope_v*(x - offsetX) + m*stepY + O_y + extra = int(math.ceil((abs(yTop) + abs(yBottom)) / stepY + (width + abs(offsetX)) * slope_v / stepY)) + 2 + m_min = int(math.floor((yBottom - O_y) / stepY)) - extra + m_max = int(math.ceil((yTop - O_y) / stepY)) + extra for m in range(m_min, m_max + 1): - base_y = m * stepY + y_origin + base_y = m * stepY + O_y # "/" - y_x0 = base_y - y_xW = slope_v * width + base_y + y_x0 = slope_v * (0.0 - offsetX) + base_y + y_xW = slope_v * (width - offsetX) + base_y y_lo, y_hi = min(y_x0, y_xW), max(y_x0, y_xW) if not (y_hi < yBottom or y_lo > yTop): self._addGapLine(path, 0.0, y_x0, width, y_xW, gap) # "\" - y_x0b = base_y - y_xWb = -slope_v * width + base_y + y_x0b = -slope_v * (0.0 - offsetX) + base_y + y_xWb = -slope_v * (width - offsetX) + base_y y_lo2, y_hi2 = min(y_x0b, y_xWb), max(y_x0b, y_xWb) if not (y_hi2 < yBottom or y_lo2 > yTop): self._addGapLine(path, 0.0, y_x0b, width, y_xWb, gap) @@ -1250,35 +1342,37 @@ def _nearestGridCoord(self, value, origin, mainStep, mainGap, subStep, subGap): return best[1] if best is not None else value @objc.python_method - def _triFamilies(self, orient, stepX, stepY, O_y): + def _triFamilies(self, orient, stepX, stepY, O_y, offsetX=0.0, offsetY=0.0): """Return 3 line families as (ux, uy, spacing, phase) for a tri-grid. Each family is defined by its unit normal (ux, uy) and the series t = phase + k * spacing (k integer) where t = ux*x + uy*y is the projection of a point onto the normal. Gap-offset lines lie at t = phase + k*spacing ± gap/2. + offsetX/offsetY shift the grid origin from (0, O_y) to (offsetX, O_y+offsetY). """ + O_y = O_y + offsetY if orient == 'horizontal': - slope = 2.0 * stepY / stepX # slope of diagonal lines + slope = 2.0 * stepY / stepX sq = math.sqrt(1.0 + slope * slope) return [ # horizontal lines: normal (0,1), phase = O_y, spacing = stepY (0.0, 1.0, stepY, O_y), - # "/" lines: using normal (slope, -1)/sq so t increases with m - (slope / sq, -1.0 / sq, slope * stepX / sq, -O_y / sq), - # "\" lines: normal (slope, 1)/sq, phase = O_y/sq - (slope / sq, 1.0 / sq, slope * stepX / sq, O_y / sq), + # "/" lines: normal (slope, -1)/sq; diagonals pass through (offsetX, O_y) + (slope / sq, -1.0 / sq, slope * stepX / sq, (slope * offsetX - O_y) / sq), + # "\" lines: normal (slope, 1)/sq + (slope / sq, 1.0 / sq, slope * stepX / sq, (slope * offsetX + O_y) / sq), ] else: # vertical - slope_v = stepY / (2.0 * stepX) # slope of diagonal lines + slope_v = stepY / (2.0 * stepX) sq_v = math.sqrt(1.0 + slope_v * slope_v) return [ - # vertical lines: normal (1,0), phase = 0, spacing = stepX - (1.0, 0.0, stepX, 0.0), - # "/" lines: normal (-slope_v, 1)/sq_v, phase = O_y/sq_v - (-slope_v / sq_v, 1.0 / sq_v, stepY / sq_v, O_y / sq_v), - # "\" lines: normal (slope_v, 1)/sq_v, phase = O_y/sq_v - ( slope_v / sq_v, 1.0 / sq_v, stepY / sq_v, O_y / sq_v), + # vertical lines: normal (1,0), phase = offsetX, spacing = stepX + (1.0, 0.0, stepX, offsetX), + # "/" lines: normal (-slope_v, 1)/sq_v; diagonals pass through (offsetX, O_y) + (-slope_v / sq_v, 1.0 / sq_v, stepY / sq_v, (-slope_v * offsetX + O_y) / sq_v), + # "\" lines: normal (slope_v, 1)/sq_v + ( slope_v / sq_v, 1.0 / sq_v, stepY / sq_v, ( slope_v * offsetX + O_y) / sq_v), ] @objc.python_method @@ -1486,6 +1580,9 @@ def _defaultSettings(self): 'subGapY': 0.0, 'gapSyncHV': False, 'gapSyncMainSub': False, + 'offsetX': 0.0, + 'offsetY': 0.0, + 'offsetSyncHV': False, } @objc.python_method @@ -1523,6 +1620,9 @@ def _getSettings(self, font): 'subGapY': float(d.get(p + '.subGapY', 0.0)), 'gapSyncHV': bool(d.get(p + '.gapSyncHV', False)), 'gapSyncMainSub': bool(d.get(p + '.gapSyncMainSub', False)), + 'offsetX': float(d.get(p + '.offsetX', 0.0)), + 'offsetY': float(d.get(p + '.offsetY', 0.0)), + 'offsetSyncHV': bool(d.get(p + '.offsetSyncHV', False)), } @objc.python_method @@ -1551,6 +1651,9 @@ def _saveSettings(self, font, s): d[p + '.subGapY'] = s.get('subGapY', 0.0) d[p + '.gapSyncHV'] = s.get('gapSyncHV', False) d[p + '.gapSyncMainSub'] = s.get('gapSyncMainSub', False) + d[p + '.offsetX'] = s.get('offsetX', 0.0) + d[p + '.offsetY'] = s.get('offsetY', 0.0) + d[p + '.offsetSyncHV'] = s.get('offsetSyncHV', False) @objc.python_method def _loadPrefs(self): From 76c7c9a29884c775b6544b633857b77236d522b2 Mon Sep 17 00:00:00 2001 From: Palf Date: Mon, 29 Jun 2026 05:23:24 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20=E3=82=A4=E3=82=BF=E3=83=AA?= =?UTF-8?q?=E3=83=83=E3=82=AF=E8=A7=92=E5=BA=A6=E8=BF=BD=E9=9A=8F=E3=82=AA?= =?UTF-8?q?=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=A8=E4=B8=89=E8=A7=92?= =?UTF-8?q?=E5=BD=A2=E6=AD=A3=E4=B8=89=E8=A7=92=E5=BD=A2=E5=8C=96=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit followItalic を追加し、OFF 時は直交グリッドを斜め編集領域にクリップして描画する。 三角形グリッドは Y をメトリクス基準に固定し X を補正して、H=V 同期時に正三角形になる。 Co-authored-by: Cursor --- .../Resources/SnappingGridSettings.nib | Bin 31980 -> 32329 bytes .../Resources/SnappingGridSettings.xib | 8 +- .../Contents/Resources/plugin.py | 254 +++++++++++++----- 3 files changed, 188 insertions(+), 74 deletions(-) diff --git a/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.nib b/SnappingGrid.glyphsPlugin/Contents/Resources/SnappingGridSettings.nib index a851582055a920ba582a722c72660f79dab12e57..76c2b1e5c7ad38d389349f4739c0289745b8fdcc 100644 GIT binary patch delta 3575 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 Dv65K- delta 3242 zcmai$X>1hd8OJ^5uo>`u0lwDWHMTj5Z9R>pV|-q=l6e} z_doB9-`Umrm;V<2^hBsTdZ4}Ss;z%u!Xc!jq=?Xs)RdG4@EWuhgVxNHl+SYD(T`2* zbv5pvYkyC$3?h~ZWfA$ZNnwRV{vu+mm?%&Jny^*@UrBPXnxxQrqVNWavsR)=8->_0 zeGuzYMA|eZSb>Lpg&u{Kcqm-ziL(Zen<860A=ZXLI|e-- zikwF8^ib60(Y@C_#3rFBTsZAfS*k?-C6W|aE-@xb6j&+Afo%AE_yS4R)=CNXf<(Tp zlESu26y71lS(ikS<5GxSL~lhuf&Lo$WsIWB-AwluF=DG3`PML1GqD6~hTqN<*2c)+ z&Lq~sNZX6~Ma-?3yU_QePnC%+k;%7IR@q9K{HtV%XUPdxD^qN}ENh$L4`BVEtgud* zv@Z0Q(GSWhABE3QDCW*o+N|i_L-2FP@&=qnBdaymGk38)wJ86glAyMXg@ld(}&96u~jCs!e-wk%o&jygLo=PQ$y?@aiAZwHGc|r2!2Ni9tJBSYqnyo>V?||=2GRq_Cn8X{$pC)_{IXNYR?$Pr#oHsr(%Lc!**XAz7P-F9_q* z!irWBPOul@8^bE!34c0Fu`^*g?0zX6XQN??jDn?`O1xtqwiBc7)u?QSe}l*R9C~rdYski z6sgxkY?n^qR`jQIa!0!LAhTg$$6ykHDfDSk3a3ZotTalIvS^5Hj8b4zl(BlSAu0zO zqn=Pxlvp#g9ohl4L`m8imAq}y4`x?##05I=}(&5;zQwfTQ4; z8|FI9BzOb738uzqc1et}G%y`p1!jObU@lk)7JBj8c+ICui=0Z)VH!3&@j>;s*k3%m>tf>*$o!EtZ`oC2po zH<%JM7*fF0m|@BnxiJOXxtUC5qyhlU z5_8QG>z}1t1G8V;a?S0hTbt+i)BLpr&0mKmp($t@nt^UWHz7Baa+~G_C>2@)Erpgr z%b_$V9a;g2(8}B7o?iu%0cAp2P&SkUW~FtcF8A(ym2T}y?i4>w+fa@l@b|?xP`@sI zAN6(d2dHm~Nz|W+)A&`tC(fY$QJmp>b1xScW9}78P(NPwNGt_EG3w`H8UHZ1M_8M* zoUsb|&%|E*Pw=6*g8#LvmjAS~kk@v$@Z2s7 z*E+ZH;jRMyu(OrF-&K*8Z=}n4OV{Soe~1O>q;$`*L!YKBPl|=uUj{;!K>!OTmn>x3%48?~BWvYLfj9W~Aqx@ISN@1M;*=gB#?b%cLdaqjT10$z~2gVj3YqRlK zlg|R7r-5$A)v*Dab#O7X!nnsYKjW$tZtxkGNpSN!F0(-VXRb0q;(u^en&f9&czZ|3 z`|yWoDASx8<8eR(tFWwPf z+}`dObhxZz4&C^id}MrPqT(EurF7M19kw|Jn|1Lbwp?7B=UWEMN9U2y#YfN0Up1}? zjX0g7jKse?}~4t&WiVw zzM + @@ -445,10 +446,15 @@ +