08 декабря 2012

Просто ещё один перевод

Перевод небольшой статьи (а точнее вводного курса) о Common Lisp'е. Если кто-нибудь захочет отредактировать, дополнить --- всегда рад. В соразработчики или пулреквест.

https://github.com/filonenko-mikhail/ub-lisp

31 августа 2012

Сглаженные шрифты для Stumpwm

#lisp 11:40 <ams> lisp is used to get shit done.

Итак сглаженные шрифты в стампвм. Бетаверсия. Для использования необходимо скачать clx-truetype и stumpwm (ветка release) из моего гитхаба. Xft, FreeType НЕ требуются, пуре лисп солюшн.

  cd ~/quicklisp/local-projects
  git clone https://github.com/filonenko-mikhail/stumpwm.git
  git clone https://github.com/filonenko-mikhail/clx-truetype.git

Сделать кеш TrueType шрифтов.

  sbcl
  (ql:quickload :clx-truetype)
  (xft:cache-fonts)

Затем указать нужный шрифт в ~/.stumpwmrc

  (set-font (make-instance 'xft:font :family "Consolas" :subfamily "Regular" :size 12))

Затем запустить stumpwm из ~/.xinitrc

  exec sbcl --eval "(ql:quickload :stumpwm)" --eval "(stumpwm:stumpwm)"

Поддержка несглаженных шрифтов никуда не делась, поэтому переключится обратно на них можно строкой в ~/.stumpwmrc

  (set-font "-*-terminus-medium-r-normal-*-16-*-*-*-*-*-iso10646-1")

clx-truetype будет в следующей версии quicklisp-a.

31 июля 2012

Сглаженные курсоры для CLX

Раз пошла такая пьянка, вот ещё библиотечка для красивых курсоров для CLX. Поддерживает темы, и анимированные курсоры. https://github.com/filonenko-mikhail/clx-cursor

29 июля 2012

16 июля 2012

Снимок экрана в stumpwm

Создание снимков экрана/окна для stumpwm на чистом коммон лиспе с использованием библиотеки zpng, т.е. поддерживается экспорт только в png формат.

Использовать так для всего экрана так:

prefix : screenshot RET filename.png RET

для текущего окна так:

prefix : screenshot-window RET filename.png RET

22 июня 2012

Поиск документации по Common Lisp-овым символам

Хочу напомнить, что наиболее удобным/быстрым способом поиска документации по стандартным символам Common Lisp'а является GNU'шная система справки texinfo, которая отлично поддерживается emacs'ом.

Для того, чтобы всё заработало, нужно:

  • Скачать dpans2texi-1.05.tar.gz
  • Выполнить
    cd dpans2texi-1.05
    ./configure
    make wget
    make
    
  • Поправить емаксовый конфиг. Строку "/path/to/dpans2texi-1.05" поменять на свою.
    (require 'info-look)
    (setq Info-additional-directory-list '("/path/to/dpans2texi-1.05"))
     
    (eval-after-load "slime"
      ;; register info help (dpans2texi)
      (progn 
        (info-lookup-add-help
         :mode 'lisp-mode
         :regexp "[^][()'\" \t\n]+"
         :ignore-case t
         :doc-spec '(("(ansicl)Symbol Index" nil nil nil)))
        
        (info-lookup-add-help
         :mode 'slime-repl-mode
         :regexp "[^][()'\" \t\n]+"
         :ignore-case t
         :doc-spec '(("(ansicl)Symbol Index" nil nil nil)))
        
        (defvar slime-old-documentation-lookup-function 
          (if (boundp 'slime-documentation-lookup-function)
              slime-documentation-lookup-function))
    
        (defun slime-ansicl-lookup (symbol-name)
          (interactive (list (slime-read-symbol-name "Symbol: ")))
          (info-lookup-symbol symbol-name 'lisp-mode))
    
        (setq slime-documentation-lookup-function 'slime-ansicl-lookup)
        (setq slime-ansicl-lookup (symbol-function 'slime-ansicl-lookup))))
    

Теперь, чтобы просмотреть справку по символу, нужно всего лишь на искомом слове нажать стандартное слаймовское сочетание клавиш C-c C-d h.

09 июня 2012

Stumpwm модуль сохранения раскладок клавиатуры между окнами

Введение

Долгое время переключаясь между фаерфоксом, скайпом и емаксом испытывал неудобство в постояной необходимости переключать раскладку клавиатуры, так как состояние раскладки было глобальным для всех окон. С задачей сохранения раскладки для каждого окна прекрасно справляется программа perWindowLayoutD. Однако это целый процесс со своим циклом сообщений, съедающий пусть небольшие но ресурсы компьютера. То ли дело подключиться слаймом к stumpwm, и добавить нужный функционал. Посмотрев на исходный код вышеназванной программы, я сделал вывод о том, что для работы с раскладкой клавиатуры требуется всего пару функций: XkbGetState и XkbLockGroup, которые находятся в библиотеке Xkblib, которая входит в пакет libx11. XkbGetState возвращает текущую раскладку, XkbLockGroup устанавливает указанную. К ним-то и нужно получить доступ из stumpwm. Но сначала обо всём по порядку.

Графическая система X

Из википедии X Window System (Иксы)

X Window System использует клиент-серверную модель: X-сервер обменивается сообщениями с различными клиентскими программами. Сервер принимает запросы на вывод графики (окон) и отправляет обратно пользовательский ввод (от клавиатуры, мыши или сенсорного экрана). X-сервер может быть:

  • системной программой, контролирующей вывод видео на персональном компьютере;
  • приложением, отображающим графику в окно какой-то другой дисплейной системы;
  • выделенным компонентом аппаратного обеспечения.

Статья об Иксах http://rus-linux.net/papers/xwin/X-Window.html

http://rus-linux.net/papers/xwin/X-Window_html_m1cb95d2e.png

Архитектура системы X Window

В данном случае сервер является механизмом отрисовки окон и передаёт события от устройств ввода. А клиент, подключившись к серверу, обрабатывает присланные события и указывает что и как нарисовать.

Любое приложение, которое хочет предоставить графический интерфейс, является клиентом икс сервера.

Протокол общения между клиентом и сервером состоит из следующих пакетов:

Запрос
клиент требует нарисовать что-либо в окне или запрашивает у сервера информацию;
Ответ
сервер отвечает на запрос;
Событие
сервер сообщает клиенту о событии (например, о нажатии клавиши пользователем);
Ошибка
сервер сообщает об ошибке.

X-сервер состоит из набора расширений, каждое из которых реализует определённые функции: от прорисовки геометрических примитивов до ускорения обработки и вывода на экран трёхмерной графики с использованием возможностей видеоаппаратуры. Почти каждый из этих модулей можно отключить или настроить в конфигурационном файле.

Описание базового протокола и расширений расположены по адресу http://www.x.org/releases/X11R7.6/doc/

Стандартный протокол: http://www.x.org/releases/X11R7.6/doc/xproto/x11protocol.pdf

Помимо стандартного протокола существует расширение XKeyboard, которое позволяет более гибко работать с клавиатурой. Оно-то мне и необходимо для того, чтобы сохранять и восстанавливать раскладку.

Протокол XKeyboard: www.x.org/docs/XKB/XKBproto.pdf

Данное расширение и реализует вышеназванные функции: XkbGetState и XkbLockGroup.

Сишники и не только они могут уже готовые клиентские библиотеки: низкоуровневые libx11 или xcb, высокоуровневые gtk, qt (ну эти так за компанию привёл).

Коммон лисперы (помимо биндингов к вышеназванным библиотекам) могут использовать написаную только на лиспе библиотеку clx, которая, мне кажется, берёт начало ещё в genere. С одной стороны clx реализует только базовый протокол. С другой стороны она содержит API для создания расширений.

clx также используется менеджером окон Stumpwm. Хотя он у меня уже больше похож на окружение, чем на просто менеджер.

Итак, после беглого просмотра исходников xkblib и clx я решил, что напишу:

  • маленькую часть расширения XKeyboard для clx
  • модуль для Stumpwm, который будет сохранять раскладки между окнами

Далее будут идти подробности моей работы, но пользователи Stumpwm могут сразу перейти к разделу "Установка".

CLX

Работа с библиотекой CLX проще-простого:

  • открыть дисплей
  • выполнить рисование
  • циклично обработать события
  • закрыть дисплей

Документация: http://www.stud.uni-karlsruhe.de/~unk6/clxman/contents.html

Расширение XKeyboard для clx

В целом написание расширения для clx состоит из следующих определений:

расширение
макрос define-extension
ошибки
макросы define-condition и define-error
типы данных
макросы deftype и define-accessor
необходимые константы
defconstant
события
макросы define-event и declare-event
функции для запросов/ответов
макросы with-buffer-request, with-buffer-request-and-reply

Создаём новый проект с помощью quickproject.

В зависимостях только библиотека clx.

В папке clx-xkeyboard автоматически будут сгенерированы три файла:

xkeyboard.asd
содержит asdf систему для загрузки данной библиотеки
package.lisp
содержит определение коммон лиспового пакета
xkeyboard.lisp
содержит основные наши функции
README.txt
здесь всё понятно

Необходимо отметить, что дальнейшая работа будет происходить в clx-овом пакете :xlib, поэтому файл package.lisp, можно очистить.

Для простоты в некоторые моменты можно пользоваться xml спецификацией протокола XKeyboard: http://cgit.freedesktop.org/xcb/proto/plain/src/xkb.xml

Приступим к непосредственно рутине. В файле xkeyboard.lisp добавим:

Последняя строка добавляет простой ключевой символ в глобальную переменную features. Таким образом мы рассказываем другим лисповым системам о своём существовании, мало ли чья-нибудь программа захочет воспользоватся XKeyboard расширением.

Определяем расширение, важно написать имя заглавными буквами, также как в спецификации:

В данном виде мы указываем, что для ошибок при работе с XKeyboard использовать тип условия xkeyboard-error.

Далее определяем условие и указываем это для clx:

Определяем новые типы, сначала для коммон лиспа, затем для clx:

Вспомогательные макросы для определения синонимов типов:

Указываем для clx синонимы типов:

Каждый пакет-запрос от клиента к серверу содержит код операции или, проще говоря, номер функции. Коды операций для базового протокола прописаны жестко. Коды операций для расширений также жёстко прописаны, но при работе должны увеличиваться на величину выдаваемую сервером для каждого расширения отдельно.

Создаём список кодов для запросов:

Макрос, который будет возвращать величину приращения для номеров функций расширения.

Задаём версию расширения:

Для того, чтобы клиент мог вызывать функции расширения, он сначала для своего сеанса должен его включить. Это производится запросом UseExtension.

Его спецификация выглядит так:

SizeType or ValueName
1??opcode
10xkb-opcode
22request-length
2CARD16wantedMajor
2CARD16wantedMinor
11Reply
1BOOLsupported
2CARD16sequence number
40reply length
21serverMajor
20serverMinor
20unused

Теперь подключение к иксам может выглядеть так:

Основные два макроса для взаимодействия с сервером:

  • для отправки запроса и получения ответа with-buffer-request-and-reply
  • для только отправки запроса with-buffer-request

Macros with-buffer-request-and-reply
         (buffer opcode reply-size &key sizes multiple-reply inline)
                       type-args &body reply-forms

В аргумент buffer передаётся открытый с помощью open-display дисплей.

opcode
является приращением для номеров функций для используемого расширения.
reply-size
указывает на размер принимаемого ответа по идее в битах.
sizes
содержит размеры отправляемых полей ….
type-args
содержит формы по отправке данных запроса; может также содержать любой исполняемый код; данные будут отосланы в той последовательности, в которой вызывались формы
reply-forms
содержит формы по получению данных ответа; может также содержать любой код; при получении данных нужно указывать смещение относительно начала пакета

Итак теперь функция XkbGetState. Вот как она выглядит для пользователя Xkblib http://www.x.org/releases/X11R7.7/doc/libX11/XKB/xkblib.html#Determining_Keyboard_State

typedef struct {
      unsigned char            group;                /* effective group index */
      unsigned char            base_group;           /* base group index */
      unsigned char            latched_group;        /* latched group index */
      unsigned char            locked_group;         /* locked group index */
      unsigned char            mods;                 /* effective modifiers */
      unsigned char            base_mods;            /* base modifiers */
      unsigned char            latched_mods;         /* latched modifiers */
      unsigned char            locked_mods;          /* locked modifiers */
      unsigned char            compat_state;         /* effective group => modifiers */
      unsigned char            grab_mods;            /* modifiers used for grabs */
      unsigned char            compat_grab_mods;     /* mods used for compatibility mode grabs */
      unsigned char            lookup_mods;          /* modifiers used to lookup symbols */
      unsigned char            compat_lookup_mods;   /* mods used for compatibility lookup */
      unsigned short            ptr_buttons;         /* 1 bit => corresponding pointer btn is down */
} 
XkbStateRec
*XkbStatePtr;

Status XkbGetState ( display , device_spec , state_return )
Display * display ; /* connection to the X server */
unsigned int device_spec ; /* device ID, or XkbUseCoreKbd */
XkbStatePtr state_return ; /* backfilled with Xkb state */ 

А вот она же но в спецификации протокола:

SizeType or ValueName
1??opcode
14xkb-opcode
22request-length
2KB_DEVICESPECdeviceSpec
2unused
11Reply
1CARD8deviceID
2CARD16sequence number
40length
1SETofKEYMASKmods
1SETofKEYMASKbaseMods
1SETofKEYMASKlatchedMods
1SETofKEYMASKlockedMods
1KP_GROUPgroup
1KP_GROUPlockedGroup
2INT16baseGroup
2INT16latchedGroup
1SETofKEYMASKcompatState
1SETofKEYMASKgrabMods
1SETofKEYMASKcompatGrabMods
1SETofKEYMASKlookupMods
1SETofKEYMASKcompatLookupMods
1unused
2SETofBUTMASKptrBtnState
6unused

Начнём со структуры, которая будет возвращаться пользователю. Тип полей структуры взят из спецификации протокола.

Теперь функция:

Теперь определим функцию XkbLockGroup. Для пользователя она выглядит так: http://www.x.org/releases/X11R7.7/doc/libX11/XKB/xkblib.html#Changing_Groups

Bool XkbLockGroup ( display, device_spec, group )
Display * display ; /* connection to the X server */
unsigned int device_spec ; /* device ID, or XkbUseCoreKbd */
unsigned int group ; /* index of the keysym group to lock */ 

Внутри себя она содержит вызов XkbLatchLockState, которая описана так:

SizeType or ValueName
1??opcode
15xkb-opcode
24request-length
2KB_DEVICESPECdeviceSpec
1SETofKEYMASKaffectModLocks
1SETofKEYMASKmodLocks
1BOOLlockGroup
1KB_GROUPgroupLock
1SETofKEYMASKaffectModLatches
1SETofKEYMASKmodLatches
1unused
1BOOLlatchGroup
2INT16groupLatch

Функция не трубет ответа от сервера, поэтому воспользуемся макросом with-buffer-request

Теперь определим lock-group

Ну что же, как я уже и говорил текущую раскладку можно получить из поля locked-group структуры device-state, а установить с помощью функции lock-group.

Поэтому теперь пришло время патчить Stumpwm.

Модуль для Stumpwm

Документация Stumpwm: http://stumpwm.org/manual/stumpwm.html#NOD1

Я разместил исходный код в папке clx-xkeyboard/test/stumperwindowlayout.lisp

Проверка загрузки расширения, переключение в пакет stumpwm:

Функция для получения текущей раскладки клавиатуры:

Теперь вообще элементарные вещи остались. Определим функцию-ловушку переключения фокуса между окнами:

В stumpwm:*display* хранится текущий дисплей, для которого и выполняется задача управления окнами.

Аргументы window и previos-window содержат соответственно окна получившее и отдавшее фокус. Структура окна определена в stumpwm, нам понадобиться её поле с xlib'овской структурой — (window-xwin previous-window). У каждого окна есть свой список свойств: добавим туда свойство :keyboard-layout.

Сохранение раскладки для предыдущего окна производится формой: (setf (getf (xlib:window-plist (window-xwin previous-window)) :keyboard-layout) current-layout)

Установка раскладки для окна, получившего фокус, производится формой: (xlib:lock-group *display* :group window-layout)

Следует отметить, что при переключении между рабочими столами, фокус предыдущего окна не теряется и параметр previous-window будет равен nil. Необходимо проигнорировать эту ситуацию и отловить событие переключения между рабочими столами:

Теперь определим команды включения и выключения режима запоминания раскладки. Это делается с помощью макроса stumpwm:defcommand.

Установка

Необходимо скопировать репозитарий

git clone http://github.com/filonenko-mikhail/clx-xkeyboard.git --depth 1

А затем в ~/.stumpwmrc добавить строки:

19 марта 2012

Телевидеть

Я прошу у вас внимания,
Я нешуточная мания,
Я ура телевещанию,
Да здравствует, прогресс.

Желанья им фильтруются,
И мненья формируются,
И всякому явлению,
Оно прибавит вес.

Здесь известно всё заранее
Ей не нужны эти знания,
И без лишнего старания,
Рождается звезда.

Немного обаяния,
Запудрены барание,
Мозги у тети Мани.
Ставки, гонки, поезда.

Не коснётся пусть нас суетность,
Толстухи вальс тануцуют,
И не кстати, и не вместе,
И не в лад, и не в попад.

Уж как карты нас тусуют.
Кто не с нами пусть пасуют.
Ведь экран подчас рисует,
Жизнь прекрасней во сто крат.

Эгей, ручку поверните,
Настал свиданья час,
Мы будем телевидеть,
А просто видеть не для нас.

А вот студия другая,
Передача дорогая,
По количеству в ней выплаканных слез.

Слезы меряют на литры,
Обязательно пролиты,
Они будут, да, мой друг, что за вопрос.

Ведь за этим, сна не зная,
Редактура наблюдает,
Репетируя, когда, зачем и кто.

Кто заплачет, кто завоет.
Кто встречает, успокоит,
Кто споет, не дай бог, ежели не то.

Продлеваются контракты,
Улучшаются контакты,
Без антракта, и без устали, за трактом тракт.

Два часа рыдает зритель,
Вы программу поглядите,
И от всей души получите инфаркт.

Эгей, дежурно улыбнитесь,
Поправим парики,
Мы будем телевидеть,
А кто не телевидет, те все дураки.

Вот дама в платье из московшвея.
Правый бок чуть-чуть левее.
Левый бок чуть-чуть правее.
Дама песенки поет.

А программу переключим,
Очевидному научат,
Как оно невероятно,
Просто даже за душу берет.

Для того, чтоб телевидеть,
Можно даже еле видеть,
Можно даже еле слышать,
Полудумать, полуспать.

Мир животных, мелодрама,
Шайба, кинопанорама,
И свои зады от кресел,
Мы не в силах оторвать.

Радость, боль, любви капризы,
Все заменит телевизор,
В стороне друзья, и книги, и прогулки под луной

Кто куда, а я к дивану,
К недалёкому экрану,
Кто далёк, тем не по пути со мной

(Еще два раза)

Градский А.

17 марта 2012

Emacs и грамотность.

Я считаю, что правила русского языка соблюдать надо. Даже в неформальщине такой, как интернет. Это как фреймворк - его предки придумали, чтобы мы через N тысяч лет и в космос полететь смогли. Но кривые руки и горячая голова правилам языка следовать не хотят, поэтому метод излечения такой.

  • Установить aspell (программа проверки орфографии).
  • Например, для арчика, так:
    sudo pacman -S aspell aspell-en aspell-ru
    
  • Влить английский словарь внутрь русского, чтобы этим симбиозом потом проверять тексты на двух языках.
  • Взято здесь: http://the-bosha.ru/2010/01/12/aspell-two-language/
    su
    cd /usr/lib/aspell
    grep '^special' en.dat >>ru.dat
    aspell dump master en >w.en
    aspell dump master ru-yo >w.ru
    cat w.ru w.en >w.all
    aspell --lang=ru --encoding=UTF-8 create master ruen.rws < w.all
    rm -f w.ru w.en w.all
    echo "add ruen.rws" > ru.multi
    exit
    
  • Настроить emacs.
  • Можно примерно так:
    (require 'flyspell)
    (require 'ispell)
    
    (setq
     ispell-program-name "aspell"
     ispell-extra-args '("--sug-mode=ultra"))
    
    (setq ispell-highlight-face (quote flyspell-incorrect))
    (setq ispell-have-new-look t)
    (setq ispell-enable-tex-parser t)
    (add-hook 'text-mode-hook 'flyspell-mode)
    (add-hook 'latex-mode-hook 'flyspell-mode)
    (setq flyspell-delay 1)
    (setq flyspell-always-use-popup t)
    
    (setq flyspell-issue-welcome-flag nil)
    

Ну вот и все. Теперь можно какой-нибудь текст и перепроверить себя выполнив команды:

M+x ispell-change-dictionary RET
ru RET
M+x ispell

ispell будет подсвечивать некорректные по его мнению слова. Для коррекции ситуации можно пользоваться следующими клавишами:

  • i
  • Вставить слово в пользовательский словарь и никогда больше не считать его некорректным.
  • r
  • Вручную заменить слово.
  • Цифра или знак
  • Вставить слово из предлагаемых вариантов.
  • Пробел
  • Пропустить слово в этой проверке.

15 марта 2012

Публикуем LaTeX книгу в html формате в вебе с помощью tex4ht

Захотелось мне тут след в истории common lisp'а оставить и взялся я книгу Common Lisp The Language 2 edition переводить. Гай Стил исходный LaTeX код книги опубликовал и даже html версию выпустил. Однако html версия хоть и была сгенерирована, но еще вручную и доработана. А так как русскую html версию мне тоже надо было сделать, а руками работать не хотелось, начал я темой конвертации LaTeX в html и интересоваться.

Условно программы для преобразования можно разделить на два вида:

  • Модифицируют поведение TeX для вывода в html формат
  • Сюда можно отнести tex4ht и hyperlatex
  • Сами обрабатывают и преобразовывают исходный LaTeX/TeX код книги
  • Latex2Html, HeVeA и другие
После использования некоторых из них, я сделал вывод о том, что более всего мне подходит TeX4ht.

TeX4ht

TeX4ht примечателен тем, что не требует каких-либо вмешательств в итоговый html код, позволяя достаточно гибко и почти понятно конфигурировать процесс преобразования и достигать необходимого внешнего вида результата. А именно:

  • позволяет задавать окружающие html теги для tex заданных команд и окружений
  • редактировать css

TeX4ht понимает все команды определенные пользователем, потому что является драйвером для TeX'а (как например, pdflatex). Кроме того, TeX4ht умеет работать с такими модулями, как makeind, multind, hyperref и т.д.

Все модули можно просмотреть такой командой (подправьте для вашего дистрибутива):

ls -A /usr/share/texmf-dist/tex/generic/tex4ht/

Но программа не может состоять из одних только достоинств, поэтому TeX4ht отличает слегка нетривиальной настройкой. Я, честно говоря, не всегда представляю, что там твориться под капотом, поэтому некоторые вещи буду объяснять кратко или просто копировать текст из официальной документации.

Примеры которые здесь будут рассматриваться взяты из вышеназванной книги, исходники которой можно посмотреть здесь: http://github.com/filonenko-mikhail/cltl2-doc-ru. А результат здесь: http://filonenko-mikhail.github.com/cltl2-doc-ru.
Кроме того, есть пример попроще. Я когда-то распознал статью Лотфи Заде о нечетких множествах. https://github.com/filonenko-mikhail/lotfi-zadeh-fuzzy-sets/blob/master/Zadeh-1965.en.tex

Запуск TeX4ht

Источник на английском: http://tug.org/applications/tex4ht/mn-commands.html

Команда запуска имеет следующий шаблон:

htlatex filename "options1" "option2" "options3" "options4"

options1 содержит перечисление настроек через запятую. Обязательно начинается с команды html, xhtml или имени конфигурационного файла. Затем можно указать деление html файлов в соответствии с глубиной деления частей книги (главы, разделы).

options2 и options3 содержат перечисление настроек через пробел. Эти настройки указываются для постобработчиков tex4ht.c and t4ht.c,

options4 содержит настройки передающиеся компилятору LaTeX.

Можете просто для своего tex файла запустить процесс так:

htlatex filename.tex

и посмотреть готовый результат. Но для более тонкой настройки, я запускаю процесс так:

htlatex clm.tex 'cltl,3,next,charset=utf-8' ' -cunihtf -utf8' '' '-interaction=batchmode'

Это значит скомпилировать clm.tex. Использовать конфигурационный файл cltl.cfg. Разделить html файлы по трем уровням вложенности глав, разделов. Установить кодировку в utf-8. Затем в постобработчик также указывается кодировка utf8. А также параметр для latex, для игнорирования ошибок и работы в пакетном режиме.

Конфигурирование TeX4ht

Источник на английском: http://tug.org/applications/tex4ht/mn11.html

Как я уже сказал, конфигурационный файл устанавливается в первой позиции options1 (второй параметр для htlatex) без указания расширения файла. А расширение должно в свою очередь быть таким: cfg. Файл конфигурации содержит TeX команды и имеет следующий шаблон:

\Preamble{html}
\begin{document}
....
настроечные команды
....
\EndPreamble

В данном конфигурационном файле можно указать css для итогового документа. Для этого служат следующие команды:

\Css{content}
\Css content\EndCss
\CssFile[list-of-css-files]content\EndCssFile

Например,

 \Css{
    table.tabbing {
        border-width: 1px;
        border-spacing: 1px;
        border-style: none;
        border-collapse: collapse;
    }
    .centerline {
        text-align: left
    }
    tt {
        font-family: monospace,monospace;
    }
    div.lisp {
        color: olive;
        padding: 1em
    }
}

Можно конфигурировать html вывод для LaTeX окружений. Для этого используются функция вывода html тегов \HCode и команда конфигурации окружения \ConfigureEnv{environment-name} {before-environment} {after-environment} {before-list} {after-list}. Например:

\ConfigureEnv{lisp} {\Tg<div class=lisp>} {\Tg</div>} {} {}
\ConfigureEnv{defun} {\Tg<div class=defun>} {\Tg</div>} {} {}

Условное выполнение TeX4ht кода

Иногда необходимо выполнить некоторый TeX код только при условии использования TeX4ht. Например для TeX4ht необходимо явно указывать расширение графического файла. Это можно сделать определив две версии команды для включения илююстраций (\includefigure)

\ifx \HCode\Undef
\newcommand{\includefigure}[1]{\center{\includegraphics[width=1\linewidth]{#1}}}
\else
\newcommand{\includefigure}[1]{\center{\includegraphics[width=1\linewidth]{#1.png}}}
\fi

Строка \ifx \HCode\Undef просто проверяет наличие TeX4ht команды \HCode и выполняет одно из двух действий.

Создание предметного указателя

В результате выполнения команды htlatex, будет создан *.idx файл для генерации индекса. Следующая команда создаст *.ind файл, который и будет использован в итоговом документе.

tex '\def\filename{{FILENAME}{idx}{4dx}{ind}} \input  idxmake.4ht' 
makeindex -o FILENAME.ind FILENAME.4dx 

Библиография

Библиография и ссылки поддерживаются.

Иллюстрации

TeX4ht поддерживает вставку иллюстрации с помощью команд пакета graphicx, но в этом случае расширение графического файла необходимо указать явно.

13 марта 2012

Статус перевода cltl2ed.

Обновлено 19.06
23 главы переведена и требуют вычитки. У меня глаз замылен, поэтому если видите английский стиль изложения в русском переводе, всячески информируйте меня об этом: почтой, или багтрекером, или комментариями, или форком.

Книга оформлена для онлайн чтения в браузере. Очень удобно, например, параллельно repl-у открыть буфер с текстом в emacs-w3m.

1 Вступление
2 Типы данных
3 Область и продолжительность видимости
4 Спецификаторы типов
5 Структура программы
6 Предикаты
7 Управляющие конструкции
8 Макросы
9 Декларации
10 Символы
11 Пакеты
13 Строковые символы
14 Последовательности
15 Списки
16 Хеш-таблицы
17 Массивы
18 Строки
19 Структуры
20 Вычислитель
21 Потоки
27 Объектная система Common Lisp'а
28 Условия

Напомню, что почитать онлайн можно здесь: http://filonenko-mikhail.github.com/cltl2-doc/

А можно и pdf'ку скачать, правда качества вёрстки я не гарантирую.

С радостью приму любую критику.

07 февраля 2012

Common Lisp the Language, 2nd Edition. by Guy L. Steele Jr.

Сделал зеркало данной книги на гитхабе. Модернизированные latex исходники взял здесь: http://git.androdna.com/?p=lisp/cltl2, соответственно благодарю Daniel'а Herring'а. Наладил генерацию latex->html с помощью tex4ht. Раньше использовалась latex2html, но на сегодняшний день она слегка хромает. htlatex (оболочка над tex4ht) достаточно, кстати, хорошо справляется с задачей. Просто конфиги слегка неинтуитивные.

Ссылка: http://filonenko-mikhail.github.com/cltl2-doc/index.html

Немного перевел введение, чтобы молодежи было понятнее, поэтому если кто-то может помочь и перевести еще пару абзацев, очень буду рад. Для этого нужно всего лишь:

Там дальше я уже сам подправлю latex исходники.

02 февраля 2012

Stumpwm и mpd.

Когда сидел в полноэкранном режиме в stumpwm в emacs'е, захотелось песню, играющую из mpd послушать еще раз. Напомню, что в stumpwm управление осуществляется с помощью последовательностей клавиатурных сочетаний, как emacs'е (да-да, это не какие-то там горячие клавиши). По умолчанию префиксом сочетаний является ctrl-t. Кроме них, stumpwm также отображает всплывающие окна для информации или для ввода команды. Так вот для управления mpd уже все сделано.

Ctrl-t : служит для выполнения команд stumpwm. Эти команды в конфиге (~/.stumpwmrc) задаются с помощью макроса (defcommand). Нам понадобится стандартная команда load-module, которая загружает stumpwm модуль, который может натворить еще кучу команд.

Короче, загрузим stumpwm модуль mpd:

ctrl-t :
# команда load-module
load-m <TAB> # дада автодополнение
mpd <RET>

Подключимся из stumpwm к mpd (демону):

ctrl-t :
mpd-connect <RET>

Ну, собственно, переключение к предыдущей песне:

ctrl-t :
mpd-prev <RET>

Понадобилась пауза для того, чтобы посмотреть youtube.

ctrl-t :
mpd-toggle-pause <RET>

Emacs. Англорусский переводчик.

Update dictd.xdsl.by лежит. Нужно использовать dict.mova.org

Хочу отметить, что из под emacs'а можно пользоваться такими вот словарями, как dict. Это словарь, а не переводчик. Развивает думалку в отличие от google-translate, да еще и удобно в emacs'е пользоваться, тем более с моими конфигами :)

Понадобится пакет dictd - это самостоятельная консольная программа. Поищите в пакетах:

* sudo pacman dictd

или здесь http://sourceforge.net/projects/dict/files/dictd/:

Следующим шагом скачайте emacs пакет dictem по адресу: http://sourceforge.net/projects/dictem/files/

Как настроить пакет смотрите в официальном README. Я же приведу свой конфиг, который позволяет добится следующего цикла работы:

Бегаете по переводимому тексту. На незнакомом слове нажали C-c d и в отдельном буфере в отдельном окне отображается перевод. Между тем фокус остается там же где и был, на некотором слове. Переместились к следующему непонятному слову, нажали C-c d, и в уже открытом окне с буфером появится перевод нового слова.

Пришлось немного поколдовать, так как dictem упорно плодил лишние буферы.

(require 'dictem)

(setq dictem-use-existing-buffer t)

(setq dictem-use-user-databases-only t)

;;; redefined function
(defun dictem-ensure-buffer ()
  "If current buffer is not a dictem buffer, create a new one."
  (let* ((dictem-buffer (get-buffer-create dictem-buffer-name))
         (dictem-window (get-buffer-window dictem-buffer))
         (window-configuration (current-window-configuration))
         (selected-window (frame-selected-window)))
    (if (window-live-p dictem-window)
        (select-window dictem-window)
      (switch-to-buffer-other-window dictem-buffer))

    (if (dictem-mode-p)
        (progn
              (if dictem-use-content-history
                  (setq dictem-content-history
                        (cons (list (buffer-substring
                                     (point-min) (point-max))
                                    (point)) dictem-content-history)))
              (setq buffer-read-only nil)
              (erase-buffer))
      (progn 
        (dictem-mode)

        (make-local-variable 'dictem-window-configuration)
        (make-local-variable 'dictem-selected-window)
        (make-local-variable 'dictem-content-history)
        (setq dictem-window-configuration window-configuration)
        (setq dictem-selected-window selected-window)))))

(setq dictem-server "dict.mova.org")
(setq dictem-exclude-databases '("ger-" "-ger" "fra-" "-fra"))

(dictem-initialize)

(add-hook 'dictem-postprocess-match-hook
   'dictem-postprocess-match)

(add-hook 'dictem-postprocess-definition-hook 
   'dictem-postprocess-definition-separator)

(add-hook 'dictem-postprocess-definition-hook 
   'dictem-postprocess-definition-hyperlinks)

(add-hook 'dictem-postprocess-show-info-hook
   'dictem-postprocess-definition-hyperlinks)

(add-hook 'dictem-postprocess-definition-hook
   'dictem-postprocess-each-definition)

(setq dictem-user-databases-alist
  '(("_en-ru"  . ("mueller7" "korolew_en-ru" "en-ru")); "dict://dict.org:2628/web1913"))
     ("_en-en"  . ("foldoc" "gcide" "wn"))
     ("_ru-ru"  . ("beslov" "ushakov" "ozhegov" "brok_and_efr"))
     ("_ru-en" . ("ru-en"))
     ("_unidoc" . ("susv3" "man" "info" "howto" "rfc"))
     ))

(define-key dictem-mode-map [tab] 'dictem-next-link)
(define-key dictem-mode-map [(backtab)] 'dictem-previous-link)

;;; http://paste.lisp.org/display/89086
(defun dictem-run-define-at-point-with-query ()
  "Query the default dict server with the word read in within this function."
  (interactive)
  (let* ((default-word (thing-at-point 'symbol))
         (default-prompt (concat "Lookup Word "
                                 (if default-word
                                     (concat "(" default-word ")") nil)
                                 ": "))
         (dictem-query
          (funcall #'(lambda (str)
                       "Remove Whitespace from beginning and end of a string."
                       (replace-regexp-in-string "^[ \n\t]*\\(.*?\\)[ \n\t]*$"
                                                 "\\1"
                                                 str))
                   (read-string default-prompt nil nil default-word))))
    (if (= (length dictem-query) 0) nil
      (dictem-run 'dictem-base-search "_en-ru" dictem-query "."))))

(defun dictem-run-define-at-point ()
  "dictem look up for thing at point"
  (interactive)
  (let* ((default-word (thing-at-point 'symbol))
         (selected-window (frame-selected-window))
         (dictem-query
          (funcall #'(lambda (str)
                       "Remove Whitespace from beginning and end of a string."
                       (replace-regexp-in-string "^[ \n\t]*\\(.*?\\)[ \n\t]*$"
                                                 "\\1"
                                                 str))
                   default-word)))
    (if (= (length dictem-query) 0)
        nil
      (progn
        (dictem-run 'dictem-base-search "_en-ru" dictem-query ".")
        (select-window selected-window)))))

(global-set-key "\C-cd" 'dictem-run-define-at-point)
(global-set-key "\C-zd" 'dictem-run-define-at-point-with-query)

29 января 2012

Emacs workspaces.

Когда мне вконец надоело, что каждый emacs модуль открывает свои окна, как захочет, я нашел решение этой проблемы.

Скачайте workspace.el

(load-library "workspaces.el")
(global-set-key "\C-xg" 'workspace-goto)

Теперь переключитесь на первую рабочую область так:

С-x g 1 

Настройте расположение окон с помощью С-x 0|1|2|3|b .

Теперь переключитесь на вторую рабочую область:

С-x g 2 

И так далее, всего рабочих областей от 1 до 9.

23 января 2012

Embeddable Maxima. Советы и хитрости.

Updated: 23.01.12

Почему максиму сложно интегрировать в веб

Веб по-умолчанию многопользовательский. Максима по-умолчанию однопользовательская. Сделать максиму многопользовательской можно, но нужно желание этих пользователей пользоватся ею одновременно на одном компьютере.

Посмотреть справку

(pushnew "/path/to/embeddable-maxima" asdf:*central-registry*)
(ql:quickload :embeddable-maxima)
(in-package :maxima)

Для просмотра справки необходимо вызвать функцию describe("function-name"). Данная функция в окружении лиспа определена, как $describe. В лиспе вызывать ее следует с помощью mfuncall (maxima function caller). Например, просмотр справки для функции ratsimp.

MAXIMA> (mfuncall '$describe "ratsimp")

 -- Function: ratsimp ()
 -- Function: ratsimp (, , ..., )
     Simplifies the expression  and all of its subexpressions,
     including the arguments to non-rational functions.  The result is
     returned as the quotient of two polynomials in a recursive form,
     that is, the coefficients of the main variable are polynomials in
     the other variables.  Variables may include non-rational functions
     (e.g., `sin (x^2 + 1)') and the arguments to any such functions
     are also rationally simplified.

     `ratsimp (, , ..., )' enables rational
     simplification with the specification of variable ordering as in
     `ratvars'.

     When `ratsimpexpons' is `true', `ratsimp' is applied to the
     exponents of expressions during simplification.

     See also `ratexpand'.  Note that `ratsimp' is affected by some of
     the flags which affect `ratexpand'.

     Examples:

          (%i1) sin (x/(x^2 + x)) = exp ((log(x) + 1)^2 - log(x)^2);
                                                   2      2
                             x         (log(x) + 1)  - log (x)
          (%o1)        sin(------) = %e
                            2
                           x  + x
          (%i2) ratsimp (%);
                                       1          2
          (%o2)                  sin(-----) = %e x
                                     x + 1
          (%i3) ((x - 1)^(3/2) - (x + 1)*sqrt(x - 1))/sqrt((x - 1)*(x + 1));
                                 3/2
                          (x - 1)    - sqrt(x - 1) (x + 1)
          (%o3)           --------------------------------
                               sqrt((x - 1) (x + 1))
          (%i4) ratsimp (%);
                                     2 sqrt(x - 1)
          (%o4)                    - -------------
                                           2
                                     sqrt(x  - 1)
          (%i5) x^(a + 1/a), ratsimpexpons: true;
                                         2
                                        a  + 1
                                        ------
                                          a
          (%o5)                        x


  There are also some inexact matches for `ratsimp'.
  Try `?? ratsimp' to see them.

Выполнить строку

(pushnew "/path/to/embeddable-maxima" asdf:*central-registry*)
(ql:quickload :embeddable-maxima)
(in-package :maxima)

Делай ast с помощью специальной функции macsyma-read-string, раз:

Важно не забыть один из терминальных символов ";" или "$".

MAXIMA> (macsyma-read-string "ratsimp( (2*x^2 + 3*x + 1) - (x^2 + x^2 - 2*x + 4*x + 1) );")

(($RATSIMP)
 ((MPLUS) ((MPLUS) ((MTIMES) 2 ((MEXPT) $X 2)) ((MTIMES) 3 $X) 1)
  ((MMINUS)
   ((MPLUS) ((MEXPT) $X 2) ((MEXPT) $X 2) ((MTIMES) ((MMINUS) 2) $X)
    ((MTIMES) 4 $X) 1))))

Выполняй ast с помощью вызова максимы-функции ev(expression,arg_1,arg_2,...,arg_n), два:

MAXIMA> (mfuncall '$ev *)

$X

Отображай полученное ast в строчку, три:

MAXIMA> (displa *)

x
NIL

В данном примере мы упростили выражение ((2*x^2 + 3*x + 1) - (x^2 + x^2 - 2*x + 4*x + 1)) с помощью функции ratsimp.

Простой json-rpc для maxima

Осторожно, американский стиль повествования! Верите ли вы, что я сделаю это за 42 строчки? А вот да, сделаю. Конечно это лисп, и здесь в рамках одной строки вмещается раза в два больше информации, чем в алгольных языках. При этом я еще и поиспользую кучу библиотек, хотя считается, что под cl их нет. Ладно, если не считать пустых строк, то их всего 32. Ну что поверили? А вот я опять вас обманул. Действительно полезных строк 27. Итак json-rpc сервис для maxima, для того, чтобы вы из любого языка, который умеет http и json могли решить дифференциальное уравнение второго порядка. Только здесь и только сейчас.

Загружаем три библиотеки, 1-ая строка:

(mapcar #'ql:quickload '(:embeddable-maxima :restas :cl-json))

Обозначаем restas модуль для обработки http запросов, 4-ая строка:

(restas:define-module :maxima-json-rpc
  (:use #:cl #:restas #:json #:json-rpc))
(in-package :maxima-json-rpc)

Создаем функцию для проверки: заканчивается ли переданная максима команда с помощью терминальных символов $ или ;, 11-ая строка:

(defun ensure-valid-maxima-input (input)
  "Returns string with appended ';', if input string does not have maxima command terminator at the end."
  (let* ((input-trimmed (string-trim '(#\Space #\Newline #\Tab) input))
        (last-char (char input-trimmed (1- (length input-trimmed)))))
    (if (and (not (char= #\; last-char)) (not (char= #\$ last-char)))
        (concatenate 'string input-trimmed ";")
        input-trimmed)))

Создаем функцию, которая преобразовывает строковое выражение в АСТ для максимы, 13-ая строка:

(defun maxima-ast-from-string (input)
  (maxima::macsyma-read-string (ensure-valid-maxima-input input)))

Макрос, который заставляет максиму, выводить результат в "компьютерном" синтаксисе (дроби через /, степени - ^, и т.д.), 16-ая строка:

(defmacro with-2d-output (&body body)
  `(let ((maxima::$display2d nil))
     ,@body))

Экспортируемая функция доступная из удаленных источников. Создается с помощью json-rpc:defun-json-rpc, 24-ая строка:

(defun-json-rpc evaluate :explicit (text)
  "Evaluate maxima expression"
  (let ((result (make-array '(0) :element-type 'base-char :fill-pointer 0 :adjustable t)))
    (with-output-to-string (*standard-output* result)
      (with-2d-output
        (maxima::displa
         (maxima::mfuncall 'maxima::$ev (maxima-ast-from-string text)))))
    result))

Обработчик http маршрута, например, http://127.0.0.01/jsonrpc. 31-ая строка:

(define-route jsonrpc ("jsonrpc"
                            :method :post 
                            ;;:content-type "application/json"
                            )
  "json rpc route"
  (let ((*json-rpc-version* +json-rpc-2.0+))
    (invoke-rpc (hunchentoot:raw-post-data :force-text t))))

Ну а здесь мы предоставляет простую страничку, которая умеет с помощью jquery, ajax, json-rpc решать те самые диффуры:

(define-route example ("example")
   (merge-pathnames "examples/js-maxima-rpc-client.html" (asdf:component-pathname (asdf:find-system :maxima-json-rpc))))

Последняя строка для запуска:

(restas:start '#:maxima-json-rpc :port 8080)

Да, это все оформлено и в репозитарии лежит. https://github.com/filonenko-mikhail/maxima-json-rpc

Но это еще не всё. В дополнение вы получаете простой пример на php для того, чтобы моментально находить ответы на непростые математические вопросы. Внимание, данный пример требует наличия JSON-RPC PHP!

<?php
  require_once 'jsonRPCClient.php';
  $maxima = new jsonRPCClient('http://127.0.0.1:8080/jsonrpc');
  print "Maxima evaluator\n";
  print "Evaluate ratsimp(x^2 + 2*x + 1 - (x + 1)^2)\n";
  print $maxima->evaluate("ratsimp(x^2 + 2*x + 1 - (x + 1)^2)");

  print "Evaluate x^2 + 2*x + 1 + (x + 1)^2 in environment x=2\n";
  print $maxima->evaluate("ev(x^2 + 2*x + 1 + (x + 1)^2, x=2)");
?>

И напоследок, sh и решение диффуров. Диффуры такие:

(%i1) 'diff(f,x,2) = sin(x) + 'diff(g,x);

                                2
                               d f            dg
(%o1)                          --- = sin(x) + --
                                 2            dx
                               dx
(%i2) 'diff(f,x) + x^2 - f = 2*'diff(g,x,2);

                                               2
                               2   df         d g
(%o2)                         x  + -- - f = 2 ---
                                   dx           2
                                              dx

sh файлик такой:

#!/bin/sh
curl -i -X POST -d "{\"jsonrpc\": \"2.0\", \"method\": \"evaluate\", \"params\": [\"desolve(['diff(f(x),x,2) = 'diff(g(x),x,1)+sin(x), 'diff(f(x),x,1)-f(x)+x^2 = 2*'diff(g(x),x,2)], [f(x),g(x)]);\"], \"id\": 1}" http://linkfly.ru:8181/jsonrpc

А теперь, кто готов все повторить, но уже на пайтоне?

P.S. А Firemax, что расширение для Firefox, очень даже неплохо справляется с задачей эмуляции emacs под броузером.

16 января 2012

Common Lisp. Работа со звуком. CL-PortAudio.

Сразу код: караоке.

Установите PortAudio.
Включите музыку (желательно, с вокалом), и выполните следующий код:

sudo pacman -S portaudio
git clone --depth 1 https://github.com/filonenko-mikhail/cl-portaudio.git
emacs
M+x slime
  (pushnew "path/to/cl-portaudio/" asdf:*central-registry*)
  (ql:quickload :cl-portaudio)
  (ql:quickload :cl-portaudio-tests)
  (portaudio-tests:test-read-write-echo)

Теперь у вас есть 15 секунд, чтобы подпевать. Да, можно было бы подпевать и без таких сложных манипуляци, как лисп, emacs, но в результате небольшой задержки при записи/воспроизведении собственный голос звучит из колонок, как бэквокал, и я, например, понял, почему многие просят меня не подпевать радио.

CL-PortAudio

CL-PortAudio cffi-биндинги к PortAudio. PortAudio небольшая кроссплатформенная библиотека для работы со звуком. Она позволяет:

  • записывать звук;
  • воспроизводить звук.

CL-PortAudio соответственно позволяет делать эти вещи из лиспа.

Документация скопирована и переработана.

API

PortAudio предоставляет очень простое API. Для передачи данных в обоих направлениях можно использовать функцию обратного вызова (callback) или блокирующие функции чтения/записи. Блокирующие функции, надо сказать, можно "разблокировать", с помощью вызова *available функций, которые возвращают возможность чтения/записи данных.

CL-PortAudio использует только блокирующий ввод/вывод, так как функция обратного вызова исполняется в "критическом" контексте, и в ней нельзя даже память выделить, не говоря уже об интерпретаторе лиспа.

Common Lisp позволил уменьшить код основной работы со звуком до 4 функций/макросов. Вот код караоке, которое было вначале.

 (use-package :portaudio)

 (defconstant +frames-per-buffer+ 1024)
 (defconstant +sample-rate+ 44100d0)
 (defconstant +seconds+ 15)
 (defconstant +sample-format+ :float)
 (defconstant +num-channels+ 2)


 (defun test-read-write-converted-echo ()
  "Record input into an array; Separate array to channels; Merge channels into array; Play last array." 
  (with-audio
    (format t "~%=== Wire on. Will run ~D seconds . ===~%" +seconds+) 
    (with-default-audio-stream (astream +num-channels+ +num-channels+ :sample-format +sample-format+ :sample-rate +sample-rate+ :frames-per-buffer +frames-per-buffer+) 
      (dotimes (i (round (/ (* +seconds+ +sample-rate+) +frames-per-buffer+)))
        (ignore-errors (write-stream astream
                                    (merge-channels-into-array astream
                                                               (separate-array-to-channels astream
                                                                                           (read-stream astream)))))))))

  • with-audio (&body body)

    Макрос инициализирует в начале и деинициализирует в конце body библиотеку portaudio.

  • with-default-audio-stream ((var num-input num-output &key (sample-format :float) (sample-rate 44100.0d0) (frames-per-buffer 1024)) &body body)

    Макрос открывает в начале и закрывает в конце поток для ввода/вывода с заданным количеством каналов, форматом данных (только :float), частотой и количеством данных за один вызов записи/чтения. Макрос также запускает и останавливает открытый поток.

  • read-stream (pa-stream)

    Читает данные из потока запущенного ранее. Возвращает одномерные массив данных типа sample-format (только 'single-float) размером (* frames-per-buffer num-input).

  • write-stream (pa-stream buffer)

    Записывает данные в поток запущенный ранее. Массив должен быть одномерным типа sample-format (только 'single-float) размером (* frames-per-buffer num-output).

  • separate-array-to-channels (pa-stream array)

    Преобразует одномерный массив в (кол-во_каналов)-мерный массив.

  • merge-channels-into-array (pa-stream channels)

    Преобразует (кол-во_каналов)-мерный массив в одномерный массив.

Бинарные сборки библиотеки PortAudio можно скачать вот здесь http://planet.plt-scheme.org/package-source/clements/portaudio.plt/2/3/lib/. Очень большое спасибо ракетчикам.

Протестировано:

  • SBCL 1.0.54, archlinux x86_64
  • SBCL 1.0.53.74.mswinmt.1092-207a13d, windows 7 x86_64 (vbox).
  • SBCL 1.0.55 mt, Mac OS X 10.6.8 x86 (thanks Žiga Lenarčič).

03 января 2012

Common Lisp. 3d plot renderer.

Завлекушная картинка.

Захотелось тут написать простой интерактивный просмотрщик дву- и трехмерных графиков.

Я начал с библиотеки clx. Данная библиотека реализует протокол общения с графическим X сервером и не содержит внешних зависимостей. X сервер содержит расширение glx позволяющее использовать opengl. Я думал, что расширение glx заведется. Не завелось. Хотя под windows+cygwin/x+clx успех был, но это не моя конфигурация по-умолчанию.

На канале #lisp подсказали библиотеку glop, которую можно назвать урезанным clx'ом. Данная библиотека не реализует протокол самостоятельно, а просто является оберткой над сишными клиентами, а под windows и macos использует родные api.

Сегодня пойдет речь о том, как создать рендерер графиков на коммон лиспе с помощью библиотеки glop. По сути, это будет движок для приближения/отдаления точки и вращения вокруг нее.

Сразу же код:

(defpackage :3dplot
  (:use #:cl )
  (:export #:draw-plot #:while))

(in-package #:3dplot)

(defvar *min-zoom* 0.1)
(defvar *zoom-step* 0.1)
(defvar *rotate-multiplicator* 0.3)
(defvar *viewport-multiplicator* 10)

(defclass plotwindow (glop:window)
  ((1st-pressed :initform nil :accessor 1st-pressed)
   (zoom :initform 1 :accessor zoom)
   (xangle :initform 0 :accessor xangle)
   (yangle :initform 0 :accessor yangle)))

(defmethod glop:on-event ((window plotwindow) (event glop:key-event))
  (when (eq (glop:keysym event) :escape)
    (glop:push-close-event window))
  (when (and (glop:pressed event) (eq (glop:keysym event) :f))
    (glop:toggle-fullscreen window))
  (when (and (glop:pressed event) (eq (glop:keysym event) :g))
    (glop:set-fullscreen window)))

(defmethod glop:on-event ((window plotwindow) (event glop:button-event))
  (case (glop:button event)
    (1 ;; main button
     (setf (1st-pressed window) (glop:pressed event)))
    (4 ;; scroll up
     (when (> (zoom window) *min-zoom*)
       (decf (zoom window) *zoom-step*)
       (glop:push-event window (make-instance 'glop:resize-event :height (glop:window-height window)
                                                                 :width (glop:window-width window))))) 
    (5 ;; scroll down
     (incf (zoom window) *zoom-step*)
     (glop:push-event window (make-instance 'glop:resize-event :height (glop:window-height window)
                                                               :width (glop:window-width window))))))

(defmethod glop:on-event ((window plotwindow) (event glop:mouse-motion-event))
  (when (1st-pressed window)
    (incf (xangle window) (* *rotate-multiplicator* (glop:dx event)))
    (incf (yangle window) (* *rotate-multiplicator* (glop:dy event)))))

(defmethod glop:on-event ((window plotwindow) (event glop:resize-event))
  (let* ((width (glop:width event))
         (height (glop:height event))
         (aspect-ratio (/ height width))
         (zoom (zoom window)))
    (gl:viewport 0 0 width height)
    (gl:matrix-mode :projection)
    (gl:load-identity)
    (gl:ortho
     (* zoom (- *viewport-multiplicator*))
     (* zoom *viewport-multiplicator*)
     (* zoom (- (* *viewport-multiplicator* aspect-ratio)))
     (* zoom (* *viewport-multiplicator* aspect-ratio))
     (* zoom (- *viewport-multiplicator*))
     (* zoom *viewport-multiplicator*))))

(defmacro while (condition &body body)
  (let ((var (gensym)))
    `(do ((,var nil (progn ,@body)))
         ((not ,condition) ,var))))

(defun draw-3d-axes ()
  "Draw opengl 3d axes"
  (gl:color 0 1 0)
  (gl:with-primitives :lines
    (gl:vertex -10.0 0.0 0.0)
    (gl:vertex 10.0 0.0 0.0)
    ;; arrow
    (gl:vertex 10.0 0.0 0.0)
    (gl:vertex 9.5 0.5 0.0)
    (gl:vertex 10.0 0.0 0.0)
    (gl:vertex 9.5 -0.5 0.0)

    (gl:vertex 1.0 -0.2 0.0)
    (gl:vertex 1.0 0.2 0.0))
  (gl:color 1 1 0)
  (gl:with-primitives :lines
    (gl:vertex 0.0 -10.0 0.0)
    (gl:vertex 0.0 10.0 0.0)
    ;; arrow
    (gl:vertex 0.0 10.0 0.0)
    (gl:vertex -0.5 9.5 0.0)
    (gl:vertex 0.0 10.0 0.0)
    (gl:vertex 0.5 9.5 0.0)
    ;;unit
    (gl:vertex -0.2 1.0 0.0)
    (gl:vertex 0.2 1.0 0.0))
  (gl:color 0.4 0.5 1)
  (gl:with-primitives :lines
    (gl:vertex 0.0 0.0 -10.0)
    (gl:vertex 0.0 0.0 10.0)
    ;; arrow
    (gl:vertex 0.0 0.0 10.0)
    (gl:vertex -0.5 0.0 9.5)
    (gl:vertex 0.0 0.0 10.0)
    (gl:vertex 0.5 0.0 9.5)
    ;; unit
    (gl:vertex -0.2 0 1.0)
    (gl:vertex 0.2 0 1.0)))

(defun draw-plot-points (fn x-start x-end x-step y-start y-end y-step)
  "Draw plot of given function"
  (gl:color 1 1 1)
  (do ((x x-start (+ x x-step)))
      ((< x-end x) nil)
    (gl:with-primitives :line-strip
      (do ((y y-start (+ y y-step)))
          ((< y-end y) nil)
        (gl:vertex x y (funcall fn x y)))))
  (do ((y y-start (+ y y-step)))
      ((< y-end y) nil)
    (gl:with-primitives :line-strip
      (do ((x x-start (+ x x-step)))
          ((< x-end x) nil)
        (gl:vertex x y (funcall fn x y))))))


(defun draw-plot (fn x-start x-end x-step y-start y-end y-step)
  (glop:with-window (win "Interactive 3d plot" 800 600 :win-class 'plotwindow)
    ;; GL init
    (gl:clear-color 0 0 0 0)
    ;; idle loop, we draw here anyway
    (let ((frames 0)
          (last-time (get-universal-time)))
      (while (glop:dispatch-events win :blocking nil :on-foo nil)
        ;; rendering
        (gl:matrix-mode :modelview)
        (gl:load-identity)
        (gl:scale 1 1 -1)
        (gl:clear :color-buffer)
        ;; transform view
        (gl:with-pushed-matrix 
          (gl:rotate (xangle win) 0.0 1.0 0.0)
          (gl:rotate (yangle win) 1.0 0.0 0.0)
          (gl:color 1 1 1)
          (draw-3d-axes)
          (draw-plot-points fn x-start x-end x-step y-start y-end y-step))
        (gl:flush)
        (glop:swap-buffers win)
        (incf frames)
        (when (< 1 (- (get-universal-time) last-time))
          (format *standard-output* "fps ~a~%" (/ frames  (- (get-universal-time) last-time)))
          (setf last-time (get-universal-time))
          (setf frames 0))))))

Объявляем пакет 3dplot, экспортируем из него символ draw-plot.

draw-plot fn x-start x-end x-step y-start y-end y-step

Функция принимает:

  • fn функция от двух аргументов x и y, должна вернуть числовое значение
  • x-start x-end x-step начальное, конечное и приращение аргумента x
  • y-start y-end y-step начальное, конечное и приращение аргумента y

Пример, сетчатого графика для функции z = sin(x) + cos(x), где X э [-5,5] и Y э [-5,5], шаг 0.1 (э должна быть перевернута:), можете покрутить колесиком мыши или зажав левую клавишу подвигать ею.

(ql:quickload :glop)
(ql:quickload :cl-opengl)

(3dplot:draw-plot (lambda (x y) (+ (sin x) (cos y))) -5 5 0.1 -5 5 0.1)

Почему так много скобок.

Повторюсь: то, что получилось, представляет собой небольшой 3d движок. Можно даже сказать совсем небольшой. Что в нем реализовано:

  • Фокусирование на точке (0, 0, 0)
  • Приближение к данной точке
  • Отдаление от данной точки
  • Вращение вокруг данной точки

(defvar *min-zoom* 0.1)
(defvar *zoom-step* 0.1)
(defvar *rotate-multiplicator* 0.3)
(defvar *viewport-multiplicator* 10)

Это регуляторы для нашего движка.

  • *min-zoom* минимальное расстояние, меньше которого приближаться нельзя
  • *zoom-step* скорость приближения
  • *rotate-multiplicator* скорость поворота
  • *viewport-multiplicator* глобальное увеличение

Далее класс нашего окна для рисований. Наследуем его от glop:window.

(defclass plotwindow (glop:window)
  ...

Объект такого класса будет хранить следующие параметры.

  • 1st-pressed зажата ли левая клавиша мыши
  • zoom текущее увеличение
  • xangle текущий поворот относительно оси x
  • yangle текущий поворот относительно оси y

В этот же класс можно было бы добавить глобальные константы, но это в следующий раз.

Теперь реализуем обработчики событий от нашего окна. Первый специфируемый параметер: окно, второй - событие.

(defmethod glop:on-event ((window plotwindow) (event glop:key-event)
  ....

Обрабатываем нажатые клавиши мыши:

  • ESC - выход из программы. Осуществляется отсылкой сообщения glop:close-event нашему окну.
  • f - перключение полноэкранного режима
  • g - включение полноэкранного режима

(defmethod glop:on-event ((window plotwindow) (event glop:button-event)
  ....

Обрабатываем события от мыши. Если нажата левая клавиша: устанавливаем флаг в слоте окна 1st-pressed. Если нажата scroll up, приближем сцену, если scroll down - отдаляем. Для применения приближения или отдаления отсылаем сообщение об изменениях размеров. В обработчике того события происходит настройка сцены.

(defmethod glop:on-event ((window plotwindow) (event glop:mouse-motion-event))
  ....

Обработчик события передвижения мыши. Если зажата левая (главная клавиша) увеличиваем углы поворота сцены на "пробег" мыши.

(defmethod glop:on-event ((window plotwindow) (event glop:resize-event))
  ....

Здесь обрабатываем изменение размеров окна. Кроме того, данное событие "наступает", когда пользователь воспользовался функцией приближения/отдаления. Я не буду объяснять данный код, так как в книжках по opengl (например, redbook) это сделано гораздо лучше.

(defmacro while (condition &body body)
  ....

Вспомогательный макрос.

(defun draw-3d-axes ()
  ....

Отрисовываем оси координат, единичные отрезки и даже стрелочки. Делаем это разным цветом.

  • Ось X - зеленая
  • Ось Y - желтая
  • Ось Z - синяя

(defun draw-plot-points (fn x-start x-end x-step y-start y-end y-step)
  ....

Примитивная отрисовка графика.

(defun draw-plot (fn x-start x-end x-step y-start y-end y-step)
  ....

Главная функция. Создаем окно, настраиваем цвет фона. И в цикле обработки сообщений отрисовываем нашу сцену.