on main help sign in
sigil input progress, and fix some marks
ef5db9cJohn Hyde
README.md+9−5
@@ -45,24 +45,28 @@ Regardless of what you run to develop, Vite will hot-reload code changes as you
454. Build UI and upload `glob-ames`
454. Build UI and upload `glob-ames`
465. Publish the desk from dojo: `:treaty|publish %astrolabe`
465. Publish the desk from dojo: `:treaty|publish %astrolabe`
47
47
48−### Deploying with `glob-ames` glob
48+## Deploying with `glob-ames` glob
49+Initial Setup:
491. Put the line `glob-ames+[~dister-midlev-mindyr 0v0]` in `desk.docket-0` if it's not there, where `~dister-midlevi-mindyr` is the `@p` of the distribution ship.
501. Put the line `glob-ames+[~dister-midlev-mindyr 0v0]` in `desk.docket-0` if it's not there, where `~dister-midlevi-mindyr` is the `@p` of the distribution ship.
51+
52+Every Time:
504. Build the UI with `npm run build` in the `ui` directory which will bundle all the code and assets into the `dist/` folder.
534. Build the UI with `npm run build` in the `ui` directory which will bundle all the code and assets into the `dist/` folder.
515. Upload the `dist/` folder as a glob to your distribution ship. Example url: https://distribution.urbit-ship.com/docket/upload
545. Upload the `dist/` folder as a glob to your distribution ship. Example url: https://distribution.urbit-ship.com/docket/upload
52
55
53−### Deploying with `glob-http` glob
56+## Deploying with `glob-http` glob
54−
57+Initial Setup:
55−To deploy, run `npm run build` in the `ui` directory which will bundle all the code and assets into the `dist/` folder. This can then be made into a glob by doing the following:
56−
571. Spin up a globber ship (comet or fakezod) (or just reuse your dev ship for this).
581. Spin up a globber ship (comet or fakezod) (or just reuse your dev ship for this).
582. Mount an `%globber` desk. In Dojo:
592. Mount an `%globber` desk. In Dojo:
59 1. `|merge %globber our %base` to create a new desk.
60 1. `|merge %globber our %base` to create a new desk.
60 2. `|mount %globber` to mount it.
61 2. `|mount %globber` to mount it.
61 3. Set `GLOBBER_DESK` in `.env.local` if the path to your globber desk is different.
62 3. Set `GLOBBER_DESK` in `.env.local` if the path to your globber desk is different.
63+
64+Every Time:
624. From the `ui` directory, run `npm run release` to build the ui and copy it to your globber desk
654. From the `ui` directory, run `npm run release` to build the ui and copy it to your globber desk
635. From the globber ship: `|commit %globber` (create basic mark files (see `png.hoon`) for any files without mark files)
665. From the globber ship: `|commit %globber` (create basic mark files (see `png.hoon`) for any files without mark files)
646. Now run `=dir /=garden=` to switch to the garden desk directory
676. Now run `=dir /=garden=` to switch to the garden desk directory
657. `-make-glob %globber /astrolabe` which will take the folder where you just added files and create a glob which can be thought of as a sort of bundle. It will be output to `~/zod/.urb/put`.
687. `-make-glob %globber /astrolabe` which will take the folder where you just added files and create a glob which can be thought of as a sort of bundle. It will be output to `~/zod/.urb/put`.
69+8. `=dir` to return to base desk.
668. From the `ui` directory, run `npm run get-glob` to place the latest glob in the `globs` folder.
708. From the `ui` directory, run `npm run get-glob` to place the latest glob in the `globs` folder.
679. Copy the string printed by the last step, and paste it into `desk.docket-0`.
719. Copy the string printed by the last step, and paste it into `desk.docket-0`.
688. Upload the glob in `/globs/` to any publicly available HTTP endpoint that can serve files. This allows the glob to distributed over HTTP.
728. Upload the glob in `/globs/` to any publicly available HTTP endpoint that can serve files. This allows the glob to distributed over HTTP.
desk/desk.docket-0+1−1
@@ -2,7 +2,7 @@
2 info+'Your guide to the Urbit constellations'
2 info+'Your guide to the Urbit constellations'
3 color+0x1f.243c
3 color+0x1f.243c
4 image+'https://raw.githubusercontent.com/johnhyde/astrolabe/main/ui/src/assets/astrolabe-tile.svg'
4 image+'https://raw.githubusercontent.com/johnhyde/astrolabe/main/ui/src/assets/astrolabe-tile.svg'
5− glob-http+['https://s3.us-east-2.amazonaws.com/warkgnall.com/urbit/astrolabe/globs/glob-0ve88fe.q5rn7.uh0da.ct886.b1jd4.glob' 0ve88fe.q5rn7.uh0da.ct886.b1jd4]
5+ glob-http+['https://s3.us-east-2.amazonaws.com/warkgnall.com/urbit/astrolabe/globs/glob-0v6c4an.v6o54.h391h.bas4f.pl6gv.glob' 0v6c4an.v6o54.h391h.bas4f.pl6gv]
6 base+'astrolabe'
6 base+'astrolabe'
7 version+[0 3 0]
7 version+[0 3 0]
8 website+'https://github.com/johnhyde/astrolabe'
8 website+'https://github.com/johnhyde/astrolabe'
desk/mar/ico.hoonnew file+12
@@ -0,0 +1,12 @@
1+|_ dat=@
2+++ grow
3+ |%
4+ ++ mime [/image/x-icon (as-octs:mimes:html dat)]
5+ --
6+++ grab
7+ |%
8+ ++ mime |=([p=mite q=octs] q.q)
9+ ++ noun @
10+ --
11+++ grad %mime
12+--
desk/mar/webmanifest.hoon+6−20
@@ -1,26 +1,12 @@
1−::
1+|_ dat=@
2−:::: /hoon/webmanifest/mar
3− ::
4−/? 310
5− ::
6−:::: compute
7− ::
8−=, eyre
9−=, format
10−=, html
11−|_ jon=json
12−::
13−++ grow :: convert to
2+++ grow
14 |%
3 |%
15− ++ mime [/application/json (as-octs:mimes -:txt)] :: convert to %mime
4+ ++ mime [/application/'manifest+json' (as-octs:mimes:html dat)]
16− ++ txt [(crip (en-json jon))]~
17 --
5 --
18++ grab
6++ grab
19− |% :: convert from
7+ |%
20− ++ mime |=([p=mite q=octs] (fall (rush (@t q.q) apex:de-json) *json))
8+ ++ mime |=([p=mite q=octs] q.q)
21− ++ noun json :: clam from %noun
9+ ++ noun @
22− ++ numb numb:enjs
23− ++ time time:enjs
24 --
10 --
25++ grad %mime
11++ grad %mime
26--
12--
ui/package.json+1−1
@@ -11,7 +11,7 @@
11 "build": "npm run tailwind:build && npm run build:vite",
11 "build": "npm run tailwind:build && npm run build:vite",
12 "preview": "vite preview",
12 "preview": "vite preview",
13 "check": "svelte-check --tsconfig ./tsconfig.json",
13 "check": "svelte-check --tsconfig ./tsconfig.json",
14− "_install:glob": "../bin/install.sh $GLOBBER_DESK -g",
14+ "_install:glob": "../bin/install.sh -g $GLOBBER_DESK",
15 "_install:desk": "../bin/install.sh $ASTROLABE_DESK",
15 "_install:desk": "../bin/install.sh $ASTROLABE_DESK",
16 "_install:desk:watch": "../bin/install.sh -w $ASTROLABE_DESK",
16 "_install:desk:watch": "../bin/install.sh -w $ASTROLABE_DESK",
17 "install:glob": "dotenv -c local npm run _install:glob",
17 "install:glob": "dotenv -c local npm run _install:glob",
ui/src/actions/symbolPartClick.tsnew file+28
@@ -0,0 +1,28 @@
1+const docLinkRegex = /\/~\/scry\/docs\/usr\/astrolabe\/([-~._\w\d]+).html/;
2+
3+function listenOnSvgElements(node, onPathChange) {
4+ const parts = node.querySelectorAll('path, circle, line');
5+ for (let part of parts) {
6+ if (part.getAttribute('dataisgeon') === 'true') continue;
7+ part.addEventListener('click', () => {
8+ onPathChange(part.getAttribute('part-key'));
9+ });
10+ }
11+}
12+
13+// function docLinks(node, onPathChange: Function) {
14+// convertDocLinks(node, onPathChange)
15+// return {
16+// update: (onPathChange) => convertDocLinks(node, onPathChange),
17+// };
18+// };
19+
20+export default function svgClick(node: HTMLElement, { onPartClick, enabled = true, svgString }) {
21+ if (enabled) listenOnSvgElements(node, onPartClick);
22+
23+ return {
24+ update: ({ onPartClick, enabled = true, svgString }) => {
25+ if (enabled) listenOnSvgElements(node, onPartClick);
26+ },
27+ };
28+}
ui/src/components/search/sigil/ClanSelect.svelte+1−1
@@ -28,7 +28,7 @@
28 <span class="font-mono">▼</span>
28 <span class="font-mono">▼</span>
29 </p>
29 </p>
30 {#if dropdownOpen}
30 {#if dropdownOpen}
31− <div class="absolute top-1 w-full flex flex-col mx-auto divide-y bg-white">
31+ <div class="absolute z-10 top-1 w-full flex flex-col mx-auto divide-y bg-white rounded-2xl border-b">
32 <h3 class="py-2" on:click={() => dropdownOpen = false}>
32 <h3 class="py-2" on:click={() => dropdownOpen = false}>
33 Select Sigil Type
33 Select Sigil Type
34 </h3>
34 </h3>
ui/src/components/search/sigil/GeonSelector.svelte+4−4
@@ -5,22 +5,22 @@
5 import { GEONS } from 'types/sigil';
5 import { GEONS } from 'types/sigil';
6
6
7 export let symbolQuery: SymbolQuery;
7 export let symbolQuery: SymbolQuery;
8− export let inputMode: PartType;
8+ export let popMode: Function;
9
9
10 let size = 48;
10 let size = 48;
11
11
12 function setGeon(geon) {
12 function setGeon(geon) {
13 symbolQuery.geon = geon;
13 symbolQuery.geon = geon;
14− inputMode = undefined;
14+ popMode();
15 }
15 }
16</script>
16</script>
17
17
18−<div on:click={() => inputMode = undefined}>
18+<!-- <div on:click={() => inputMode = undefined}>
19 done
19 done
20</div>
20</div>
21<div on:click={() => setGeon(undefined)}>
21<div on:click={() => setGeon(undefined)}>
22 clear
22 clear
23−</div>
23+</div> -->
24{#each GEONS as geon}
24{#each GEONS as geon}
25 <div style:width="{size}px" on:click={() => setGeon(geon)}>
25 <div style:width="{size}px" on:click={() => setGeon(geon)}>
26 <Symbol components={[geon]} {size} />
26 <Symbol components={[geon]} {size} />
ui/src/components/search/sigil/SigilInput.svelte+13−12
@@ -4,12 +4,12 @@
4 import ClanSelect from './ClanSelect.svelte';
4 import ClanSelect from './ClanSelect.svelte';
5
5
6 import type { PartType } from 'types/sigil';
6 import type { PartType } from 'types/sigil';
7− import { SigilQuery } from 'lib/sigil';
7+ import type { SigilQuery } from 'lib/sigil';
8
8
9 export let sigilQuery: SigilQuery;
9 export let sigilQuery: SigilQuery;
10 // export let clan: ('galaxy' | 'star' | 'planet') = 'planet';
10 // export let clan: ('galaxy' | 'star' | 'planet') = 'planet';
11 let focusedSymbolIndex: number = 0;
11 let focusedSymbolIndex: number = 0;
12− let inputMode: PartType = undefined;
12+ let inputComponents: string[] = [];
13
13
14 // let symbols: SymbolQuery = [];
14 // let symbols: SymbolQuery = [];
15
15
@@ -22,11 +22,6 @@
22 focusedSymbolIndex = numActiveSymbols - 1;
22 focusedSymbolIndex = numActiveSymbols - 1;
23 }
23 }
24 }
24 }
25− $: {
26− if (!focusedSymbol.geon) {
27− inputMode = 'geon';
28− }
29− }
30 // function updateSigilQuery() {
25 // function updateSigilQuery() {
31 // sigilQuery.setSymbol(focusedSymbol, focusedSymbolIndex);
26 // sigilQuery.setSymbol(focusedSymbol, focusedSymbolIndex);
32 // }
27 // }
@@ -42,15 +37,21 @@
42 <SymbolInput
37 <SymbolInput
43 bind:symbolQuery={symbol}
38 bind:symbolQuery={symbol}
44 focused={index === focusedSymbolIndex}
39 focused={index === focusedSymbolIndex}
40+ {inputComponents}
45 on:click={() => focusedSymbolIndex = index}
41 on:click={() => focusedSymbolIndex = index}
46 />
42 />
47 {/each}
43 {/each}
48 </div>
44 </div>
49 </div>
45 </div>
46+ {#if focusedSymbol}
50− <div class="bg-green-100 grow rounded-b-2xl xs:rounded-bl-none">
47+ <div class="bg-green-100 grow rounded-b-2xl xs:rounded-bl-none">
51− {inputMode}
48+ <SymbolInputModeSelector
52− <SymbolInputModeSelector bind:symbolQuery={focusedSymbol} bind:inputMode />
49+ bind:symbolQuery={focusedSymbol}
50+ bind:inputComponents
51+ unfocusSymbol={() => focusedSymbolIndex = undefined}
52+ />
53− </div>
53+ </div>
54+ {/if}
54 </div>
55 </div>
55− {sigilQuery.string}
56+ <!-- {sigilQuery.string} -->
56</div>
57</div>
ui/src/components/search/sigil/Symbol.svelte+35−2
@@ -1,15 +1,22 @@
1<script lang="ts">
1<script lang="ts">
2+ import { createEventDispatcher } from 'svelte';
2 import { sigilFromSymbols, stringRenderer as sr, symbolFromDef } from '@johnhyde/sigil-js';
3 import { sigilFromSymbols, stringRenderer as sr, symbolFromDef } from '@johnhyde/sigil-js';
3 import { sigilScalingFunction } from 'lib/sigil';
4 import { sigilScalingFunction } from 'lib/sigil';
5+ import symbolPartClick from 'actions/symbolPartClick';
4
6
5 export let components: string[];
7 export let components: string[];
6 export let size: number;
8 export let size: number;
7 export let fgColor: string = 'black';
9 export let fgColor: string = 'black';
8 export let bgColor: string = 'white';
10 export let bgColor: string = 'white';
9 export let altBgColor: string = undefined;
11 export let altBgColor: string = undefined;
12+ export let hoverColor: string = 'blue';
10 export let inverted: boolean = false;
13 export let inverted: boolean = false;
14+ export let interactive: boolean = false;
15+ export let strokeWidthFactor: number = 1;
11 let svgString: any;
16 let svgString: any;
12
17
18+ const dispatch = createEventDispatcher();
19+
13 $: {
20 $: {
14 const symbol = symbolFromDef(components);
21 const symbol = symbolFromDef(components);
15 let colors = [bgColor, fgColor];
22 let colors = [bgColor, fgColor];
@@ -25,7 +32,9 @@
25 colors,
32 colors,
26 margin: false,
33 margin: false,
27 autoScaleStrokes: true,
34 autoScaleStrokes: true,
28− strokeScalingFunctionV2: sigilScalingFunction,
35+ strokeScalingFunctionV2: (s) => {
36+ return strokeWidthFactor * sigilScalingFunction(s);
37+ },
29 },
38 },
30 );
39 );
31 svgAST.attributes.preserveAspectRatio = 'xMidYMin slice';
40 svgAST.attributes.preserveAspectRatio = 'xMidYMin slice';
@@ -34,8 +43,32 @@
34 svgString = svgString.replace(/rect fill="[^"]+"/, `rect fill="${tempAltBgColor}"`);
43 svgString = svgString.replace(/rect fill="[^"]+"/, `rect fill="${tempAltBgColor}"`);
35 }
44 }
36 }
45 }
46+
47+ function onPartClick(partId: string) {
48+ dispatch('partClick', partId);
49+ }
37</script>
50</script>
38
51
52+<style lang="scss" global>
53+ .interactive {
54+ svg {
55+ pointer-events: none;
56+ }
57+
58+ path, circle, line {
59+ pointer-events: auto;
60+
61+ &:not([dataisgeon="true"]):hover {
62+ stroke: var(--hoverColor);
63+ }
64+ }
65+ }
66+</style>
67+
39−<div class="aspect-square" style:max-width="{size}px" on:click>
68+<div class="aspect-square" class:interactive style:max-width="{size}px"
69+ style="--hoverColor: {hoverColor};"
70+ on:click
71+ use:symbolPartClick={{ onPartClick, enabled: interactive, svgString }}
72+>
40 {@html svgString}
73 {@html svgString}
41</div>
74</div>
ui/src/components/search/sigil/SymbolInput.svelte+40−4
@@ -1,20 +1,56 @@
1<script lang="ts">
1<script lang="ts">
2 import Symbol from './Symbol.svelte';
2 import Symbol from './Symbol.svelte';
3+ import without from 'lodash/without';
3
4
4− import type { SymbolQuery } from 'types/sigil';
5+ import type { PartType, SymbolQuery } from 'types/sigil';
5
6
6 export let symbolQuery: SymbolQuery;
7 export let symbolQuery: SymbolQuery;
8+ // export let inputMode: PartType;
7 export let focused: boolean = false;
9 export let focused: boolean = false;
10+ export let inputComponents: string[] = [];
8 let size = 128;
11 let size = 128;
9
12
10 $: focusedClasses = focused ? 'border border-gold-s1 border-2' : '';
13 $: focusedClasses = focused ? 'border border-gold-s1 border-2' : '';
14+
15+ function onPartDelete({ detail: partId }) {
16+ symbolQuery = symbolQuery.removePart(partId);
17+ }
18+
19+ function onPartClick({ detail: partId }) {
20+ symbolQuery = symbolQuery.addPart(partId);
21+ }
22+
23+ let filteredInputComponents = [];
24+ $: {
25+ if (inputComponents) {
26+ filteredInputComponents = without(inputComponents, ...symbolQuery.components);
27+ } else {
28+ filteredInputComponents = [];
29+ }
30+ }
11</script>
31</script>
12
32
13−<div class="aspect-square flex {focusedClasses}" style:max-width="{size}px" on:click>
33+<div class="aspect-square relative flex {focusedClasses}" style:max-width="{size}px" on:click>
14 {#if symbolQuery.components.length > 0}
34 {#if symbolQuery.components.length > 0}
15− <Symbol components={symbolQuery.components} {size} inverted={!symbolQuery.geon} />
35+ <Symbol components={symbolQuery.components} {size}
16− {symbolQuery.geon}
36+ inverted={!symbolQuery.geon}
37+ interactive={focused}
38+ hoverColor="red"
39+ on:partClick={onPartDelete}
40+ />
17 {:else}
41 {:else}
18 <div class="m-1 rounded-sm border-4 border-gray-500 grow"></div>
42 <div class="m-1 rounded-sm border-4 border-gray-500 grow"></div>
19 {/if}
43 {/if}
44+ {#if focused && filteredInputComponents.length}
45+ <div class="absolute inset-0 pointer-events-none">
46+ <Symbol components={filteredInputComponents} {size}
47+ strokeWidthFactor={2.5}
48+ fgColor={symbolQuery.geon ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)"}
49+ bgColor="transparent"
50+ inverted
51+ interactive
52+ on:partClick={onPartClick}
53+ />
54+ </div>
55+ {/if}
20</div>
56</div>
ui/src/components/search/sigil/SymbolInputModeSelector.svelte+201−7
@@ -5,21 +5,215 @@
5 import Symbol from './Symbol.svelte';
5 import Symbol from './Symbol.svelte';
6
6
7 import type { PartType, SymbolQuery } from 'types/sigil';
7 import type { PartType, SymbolQuery } from 'types/sigil';
8− import { PART_TYPES } from 'types/sigil';
9
8
10 export let symbolQuery: SymbolQuery;
9 export let symbolQuery: SymbolQuery;
11− export let inputMode: PartType;
10+ // export let inputMode: PartType;
11+ export let inputComponents: string[] = [];
12+ export let unfocusSymbol: Function;
12
13
14+ interface ModeDefs {
15+ [mode: string]: ModeDef
16+ }
17+ interface ModeDef {
18+ modes?: ModeDefs;
19+ displayParts?: string[];
20+ parts?: string[];
21+ }
22+
23+ const modeDefs: ModeDefs = {
24+ geon: {
25+ modes: {},
26+ displayParts: ['ghb'],
27+ },
28+ line: {
29+ modes: {
30+ mainFulls: {
31+ parts: ['lvf', 'lhf', 'lff', 'lbf'],
32+ },
33+ halvesFulls: {
34+ parts: ['lvfl', 'lvfr', 'lhft', 'lhfb', 'lfht', 'lfhb', 'lbht', 'lbhb'],
35+ },
36+ forwards: {
37+ parts: ['lff', 'lfft', 'lffb'],
38+ },
39+ backwards: {
40+ parts: ['lbf', 'lbft'],
41+ },
42+ weirds: {
43+ parts: ['lbhtf', 'lbrf', 'lfhm'],
44+ }
45+ },
46+ parts: ['lvf', 'lvfl', 'lvfr', 'lhf', 'lhft', 'lhfb', 'lff', 'lbf'],
47+ },
48+ arcCorner: {
49+ modes: {
50+ trFulls: {
51+ parts: ['atrf8', 'atrf6', 'atrf4', 'atrf2'],
52+ },
53+ trHalves: {
54+ parts: [/*'atra8',*/ 'atra6', 'atra4', 'atra2', 'atrb8', 'atrb6', 'atrb4', 'atrb2'],
55+ displayParts: ['atrb8', 'atrb6', 'atrb4', 'atrb2'],
56+ },
57+ tlFulls: {
58+ parts: ['atlf8', 'atlf6', 'atlf4', 'atlf2'],
59+ },
60+ tlHalves: {
61+ parts: ['atla8', 'atla6', 'atla4', 'atla2', 'atlb8', 'atlb6', 'atlb4', 'atlb2'],
62+ displayParts: ['atlb8', 'atlb6', 'atlb4', 'atlb2'],
63+ },
64+ blFulls: {
65+ parts: ['ablf8', 'ablf6', 'ablf4', 'ablf2'],
66+ },
67+ blHalves: {
68+ parts: ['ablb8', 'ablb6', 'ablb4', 'ablb2'],
69+ displayParts: ['ablb8', 'ablb6', 'ablb4', 'ablb2'],
70+ },
71+ brFulls: {
72+ parts: ['abrf8', 'abrf6', 'abrf4', 'abrf2'],
73+ },
74+ brHalves: {
75+ parts: ['abrb8', 'abrb6', 'abrb4', 'abrb2'],
76+ displayParts: ['abrb8', 'abrb6', 'abrb4', 'abrb2'],
77+ },
78+ },
79+ parts: ['ablf8', 'ablf6', 'ablf4', 'ablf2'],
80+ },
81+ arc: {
82+ modes: {
83+ rings: {
84+ parts: ['am081', 'am082', 'am083', 'am08s'],
85+ },
86+ diagonals: {
87+ parts: ['am741', 'am742', 'am743', 'am422', 'am423'],
88+ },
89+ topsBottoms: {
90+ parts: ['ab2', 'ab3', 'ab4', 'at1', 'at3', 'at4'],
91+ },
92+ leftsRights: {
93+ parts: ['al2', 'al3', 'al4', 'ar1', 'ar2', 'ar4'/*, 'arb2'*/],
94+ },
95+ rights: {
96+ parts: ['am642', 'am643'],
97+ },
98+ middles: {
99+ parts: ['am222', 'am223', 'am622', 'am623'],
100+ },
101+ middless: {
102+ parts: ['am142', 'am443'],
103+ },
104+ topRights: {
105+ parts: ['am021', 'am022', 'am023', 'am242', 'arb2'],
106+ },
107+ // weirds: {
108+ // parts: ['arb2'],
109+ // },
110+ },
111+ parts: ['am222', 'am223', 'ar4', 'ar2', 'ar1'],
112+ },
113+ circle: {
114+ parts: [
115+ 'cbr3mb', 'ctl3mb',
116+ 'cmmb',
117+ 'ct2mb', 'cl2mb', 'cb2mb', 'cr2mb',
118+ ],
119+ },
120+ donut: {
121+ parts: ['dbl', /*'dl',*/ 'dm', 'dtl', 'dtr'],
122+ },
123+ bezier: {
124+ parts: ['b1l1', 'b1l2', 'b1l3'],
125+ },
126+ };
127+ let modes: string[] = [];
128+ let modeDef: ModeDef = undefined;
129+ let subModeDefs: ModeDefs = modeDefs;
13 let size = 48;
130 let size = 48;
131+
132+
133+ $: {
134+ if (!symbolQuery.geon) {
135+ modes = ['geon'];
136+ }
137+ }
138+
139+ function pushMode(mode: string) {
140+ if (modeDef?.modes) {
141+ modes = [...modes, mode];
142+ } else {
143+ modes = [...modes.slice(0, -1), mode];
144+ }
145+ }
146+
147+ function popMode() {
148+ if (modes.length) {
149+ if (modeDef?.modes) {
150+ modes = modes.slice(0, -1);
151+ } else {
152+ modes = modes.slice(0, -2);
153+ }
154+ } else {
155+ unfocusSymbol();
156+ }
157+ }
158+
159+ $: {
160+ modeDef = undefined;
161+ subModeDefs = modeDefs;
162+ for (let mode of modes) {
163+ if (modeDef && modeDef.modes) {
164+ subModeDefs = modeDef.modes;
165+ }
166+ if (subModeDefs[mode]) {
167+ modeDef = subModeDefs[mode];
168+ } else {
169+ console.error(`Unknown mode: ${mode} of ${modes} in defs:`, subModeDefs);
170+ }
171+ }
172+
173+ if (modeDef && modeDef.modes) {
174+ subModeDefs = modeDef.modes;
175+ }
176+
177+ if (modeDef?.parts?.length && !modeDef.modes) {
178+ inputComponents = modeDef.parts;
179+ } else {
180+ inputComponents = [];
181+ }
182+ }
183+
184+ function clear() {
185+ if (modes.length) {
186+ symbolQuery = symbolQuery.clearPartType(modes[0] as PartType);
187+ } else {
188+ symbolQuery = symbolQuery.clear();
189+ }
190+ }
14</script>
191</script>
15
192
16<div class="grid grid-rows-2 grid-flow-col xs:grid-cols-2 xs:grid-flow-row place-content-center justify-center">
193<div class="grid grid-rows-2 grid-flow-col xs:grid-cols-2 xs:grid-flow-row place-content-center justify-center">
194+ <div
195+ class="aspect-square"
196+ on:click={popMode}
197+ >
198+ done
199+ </div>
200+ <div
201+ class="aspect-square"
202+ on:click={clear}
203+ >
204+ clear
205+ </div>
17− {#if inputMode === 'geon'}
206+ {#if modes[0] === 'geon'}
18− <GeonSelector bind:symbolQuery bind:inputMode />
207+ <GeonSelector bind:symbolQuery {popMode} />
19 {:else}
208 {:else}
20− {#each PART_TYPES as partType}
209+ <!-- {#each PART_TYPES as partType}
21− <SymbolInputModeSelectorButton {partType} focused={inputMode === partType}
210+ <SymbolInputModeSelectorButton {partType} focused={modes[0] === partType}
22− on:click={() => inputMode = partType}
211+ on:click={() => pushMode(partType)}
212+ />
213+ {/each} -->
214+ {#each Object.entries(subModeDefs) as [modeName, def]}
215+ <SymbolInputModeSelectorButton components={def.displayParts || def.parts} focused={modes.slice(-1)[0] === modeName}
216+ on:click={() => pushMode(modeName)}
23 />
217 />
24 {/each}
218 {/each}
25 {/if}
219 {/if}
ui/src/components/search/sigil/SymbolInputModeSelectorButton.svelte+6−3
@@ -4,15 +4,18 @@
4 import type { PartType, SymbolQuery } from 'types/sigil';
4 import type { PartType, SymbolQuery } from 'types/sigil';
5 import { PART_TYPES } from 'types/sigil';
5 import { PART_TYPES } from 'types/sigil';
6
6
7− export let partType: PartType;
7+ // export let partType: PartType;
8+ export let components: string[] = [];
8 export let focused: boolean = false;
9 export let focused: boolean = false;
9
10
10 let size = 48;
11 let size = 48;
11 $: focusedClasses = focused ? 'border border-gold-s1 border-2' : '';
12 $: focusedClasses = focused ? 'border border-gold-s1 border-2' : '';
13+ $: includesGeon = components.find(c => c[0] === 'g') === undefined;
12</script>
14</script>
13
15
14<div class="border-2 w-[48px] {focusedClasses}" on:click>
16<div class="border-2 w-[48px] {focusedClasses}" on:click>
17+ <Symbol {components} {size} inverted={includesGeon} />
15− {#if partType === 'geon'}
18+ <!-- {#if partType === 'geon'}
16 <Symbol components={['ghb']} {size} />
19 <Symbol components={['ghb']} {size} />
17 {:else if partType === 'line'}
20 {:else if partType === 'line'}
18 <Symbol components={['lvf', 'lvfl', 'lvfr', 'lhf', 'lhft', 'lhfb', 'lff', 'lbf']} {size} inverted />
21 <Symbol components={['lvf', 'lvfl', 'lvfr', 'lhf', 'lhft', 'lhfb', 'lff', 'lbf']} {size} inverted />
@@ -33,6 +36,6 @@
33 <Symbol components={['b1l1', 'b1l2', 'b1l3']} {size} inverted />
36 <Symbol components={['b1l1', 'b1l2', 'b1l3']} {size} inverted />
34 {:else}
37 {:else}
35 not {partType}
38 not {partType}
36− {/if}
39+ {/if} -->
37</div>
40</div>
38
41
ui/src/lib/sigil.ts+1−1
@@ -25,7 +25,7 @@ export class SigilQuery {
25 // symbol.components = ['gci', 'abr622']
25 // symbol.components = ['gci', 'abr622']
26 } else {
26 } else {
27 symbol.symbolType = 'suffix';
27 symbol.symbolType = 'suffix';
28− symbol.components = ['gsq', 'abl202']
28+ symbol.components = ['gsq', 'ablf2']
29 }
29 }
30 });
30 });
31 this.symbols = this.symbols;
31 this.symbols = this.symbols;
ui/src/types/sigil.ts+67−1
@@ -1,7 +1,10 @@
1+import without from 'lodash/without';
2+import uniq from 'lodash/uniq';
3+
1export const CLANS = ['galaxy', 'star', 'planet'] as const;
4export const CLANS = ['galaxy', 'star', 'planet'] as const;
2export type Clan = typeof CLANS[number];
5export type Clan = typeof CLANS[number];
3
6
4−export const PART_TYPES = ['geon', 'line', 'arc', 'ring', 'circle', 'donut', 'bezier'] as const;
7+export const PART_TYPES = ['geon', 'line', 'arc', 'circle', 'donut', 'bezier'] as const;
5export type PartType = typeof PART_TYPES[number];
8export type PartType = typeof PART_TYPES[number];
6
9
7export const GEONS = ['gci', 'gsq', 'ghl', 'ghr', 'ght', 'ghb', 'gq1', 'gq2', 'gq3', 'gq4', 'gfs', 'gbs'];
10export const GEONS = ['gci', 'gsq', 'ghl', 'ghr', 'ght', 'ghb', 'gq1', 'gq2', 'gq3', 'gq4', 'gfs', 'gbs'];
@@ -9,6 +12,23 @@ export type Geon = typeof GEONS[number];
9
12
10export type SymbolType = ('prefix' | 'suffix');
13export type SymbolType = ('prefix' | 'suffix');
11
14
15+function getComplementaryCircleId(circleId: string): string {
16+ let newId = circleId.replace(/m(?=.$)/, 'l');
17+ if (newId === circleId) {
18+ newId = circleId.replace(/l(?=.$)/, 'm');
19+ }
20+ circleId = newId;
21+ circleId = circleId.replace(/f$/, 'b');
22+ if (newId === circleId) {
23+ circleId = circleId.replace(/b$/, 'f');
24+ }
25+ return circleId;
26+}
27+
28+function getPartTypeIndex(partId: string): number {
29+ return PART_TYPES.findIndex(p => p[0] === partId[0]);
30+}
31+
12export class SymbolQuery {
32export class SymbolQuery {
13 symbolType: SymbolType;
33 symbolType: SymbolType;
14 components: string[];
34 components: string[];
@@ -16,6 +36,7 @@ export class SymbolQuery {
16 constructor(symbolType: SymbolType = 'suffix', components = []) {
36 constructor(symbolType: SymbolType = 'suffix', components = []) {
17 this.symbolType = symbolType;
37 this.symbolType = symbolType;
18 this.components = components;
38 this.components = components;
39+ this.cleanupParts();
19 }
40 }
20
41
21 set geon(geon: Geon) {
42 set geon(geon: Geon) {
@@ -27,4 +48,49 @@ export class SymbolQuery {
27 get geon(): Geon {
48 get geon(): Geon {
28 return this.components.find(c => c[0] === 'g');
49 return this.components.find(c => c[0] === 'g');
29 }
50 }
51+
52+ cleanupParts() {
53+ this.components = uniq(this.components);
54+ this.components = this.components.sort((c1, c2) => {
55+ let i1 = getPartTypeIndex(c1);
56+ let i2 = getPartTypeIndex(c2);
57+ let diff = i1 - i2;
58+ if (diff !== 0) return diff;
59+ if (c1[0] === 'c') {
60+ return c1.localeCompare(c2);
61+ } else {
62+ return c2.localeCompare(c1);
63+ }
64+ });
65+ }
66+
67+ clearPartType(partType: PartType) {
68+ this.components = this.components.filter(c => c[0] !== partType[0]);
69+ return this;
70+ }
71+
72+ clear() {
73+ this.components = [];
74+ return this;
75+ }
76+
77+ addPart(partId: string) {
78+ this.components = [...this.components, partId];
79+ if (partId[0] === 'c') {
80+ const otherPartId = getComplementaryCircleId(partId);
81+ this.components = [...this.components, otherPartId];
82+ }
83+ this.cleanupParts();
84+ return this;
85+ }
86+
87+ removePart(partId: string) {
88+ this.components = without(this.components, partId);
89+ if (partId[0] === 'c') {
90+ const otherPartId = getComplementaryCircleId(partId);
91+ this.components = without(this.components, otherPartId);
92+ }
93+ this.cleanupParts();
94+ return this;
95+ }
30}
96}