on main help sign in
sigil input progress, and fix some marks
commitef5db9c239f750d89a5e29cecfad5029732197e7 authorJohn Hyde <johnhydefour@gmail.com> dateTue 16 Aug 2022, 17:14 (4 years ago) parent17678c0
15 files changed+425−62
README.md+9−5 desk/desk.docket-0+1−1 desk/mar/ico.hoon+12 desk/mar/webmanifest.hoon+6−20 ui/package.json+1−1 ui/src/actions/symbolPartClick.ts+28 ui/src/components/search/sigil/ClanSelect.svelte+1−1 ui/src/components/search/sigil/GeonSelector.svelte+4−4 ui/src/components/search/sigil/SigilInput.svelte+13−12 ui/src/components/search/sigil/Symbol.svelte+35−2 ui/src/components/search/sigil/SymbolInput.svelte+40−4 ui/src/components/search/sigil/SymbolInputModeSelector.svelte+201−7 ui/src/components/search/sigil/SymbolInputModeSelectorButton.svelte+6−3 ui/src/lib/sigil.ts+1−1 ui/src/types/sigil.ts+67−1
README.md+9−5
@@ -45,24 +45,28 @@ Regardless of what you run to develop, Vite will hot-reload code changes as you
45454. Build UI and upload `glob-ames`
46465. Publish the desk from dojo: `:treaty|publish %astrolabe`
4747
48−### Deploying with `glob-ames` glob
48+## Deploying with `glob-ames` glob
49+Initial Setup:
49501. 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:
50534. Build the UI with `npm run build` in the `ui` directory which will bundle all the code and assets into the `dist/` folder.
51545. Upload the `dist/` folder as a glob to your distribution ship. Example url: https://distribution.urbit-ship.com/docket/upload
5255
53−### Deploying with `glob-http` glob
54−
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−
56+## Deploying with `glob-http` glob
57+Initial Setup:
57581. Spin up a globber ship (comet or fakezod) (or just reuse your dev ship for this).
58592. Mount an `%globber` desk. In Dojo:
5960 1. `|merge %globber our %base` to create a new desk.
6061 2. `|mount %globber` to mount it.
6162 3. Set `GLOBBER_DESK` in `.env.local` if the path to your globber desk is different.
63+
64+Every Time:
62654. From the `ui` directory, run `npm run release` to build the ui and copy it to your globber desk
63665. From the globber ship: `|commit %globber` (create basic mark files (see `png.hoon`) for any files without mark files)
64676. Now run `=dir /=garden=` to switch to the garden desk directory
65687. `-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.
66708. From the `ui` directory, run `npm run get-glob` to place the latest glob in the `globs` folder.
67719. Copy the string printed by the last step, and paste it into `desk.docket-0`.
68728. 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 @@
22 info+'Your guide to the Urbit constellations'
33 color+0x1f.243c
44 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]
66 base+'astrolabe'
77 version+[0 3 0]
88 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−::
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
1+|_ dat=@
2+++ grow
143 |%
15− ++ mime [/application/json (as-octs:mimes -:txt)] :: convert to %mime
16− ++ txt [(crip (en-json jon))]~
4+ ++ mime [/application/'manifest+json' (as-octs:mimes:html dat)]
175 --
186++ grab
19− |% :: convert from
20− ++ mime |=([p=mite q=octs] (fall (rush (@t q.q) apex:de-json) *json))
21− ++ noun json :: clam from %noun
22− ++ numb numb:enjs
23− ++ time time:enjs
7+ |%
8+ ++ mime |=([p=mite q=octs] q.q)
9+ ++ noun @
2410 --
2511++ grad %mime
2612--
ui/package.json+1−1
@@ -11,7 +11,7 @@
1111 "build": "npm run tailwind:build && npm run build:vite",
1212 "preview": "vite preview",
1313 "check": "svelte-check --tsconfig ./tsconfig.json",
14− "_install:glob": "../bin/install.sh $GLOBBER_DESK -g",
14+ "_install:glob": "../bin/install.sh -g $GLOBBER_DESK",
1515 "_install:desk": "../bin/install.sh $ASTROLABE_DESK",
1616 "_install:desk:watch": "../bin/install.sh -w $ASTROLABE_DESK",
1717 "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 @@
2828 <span class="font-mono">▼</span>
2929 </p>
3030 {#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">
3232 <h3 class="py-2" on:click={() => dropdownOpen = false}>
3333 Select Sigil Type
3434 </h3>
ui/src/components/search/sigil/GeonSelector.svelte+4−4
@@ -5,22 +5,22 @@
55 import { GEONS } from 'types/sigil';
66
77 export let symbolQuery: SymbolQuery;
8− export let inputMode: PartType;
8+ export let popMode: Function;
99
1010 let size = 48;
1111
1212 function setGeon(geon) {
1313 symbolQuery.geon = geon;
14− inputMode = undefined;
14+ popMode();
1515 }
1616</script>
1717
18−<div on:click={() => inputMode = undefined}>
18+<!-- <div on:click={() => inputMode = undefined}>
1919 done
2020</div>
2121<div on:click={() => setGeon(undefined)}>
2222 clear
23−</div>
23+</div> -->
2424{#each GEONS as geon}
2525 <div style:width="{size}px" on:click={() => setGeon(geon)}>
2626 <Symbol components={[geon]} {size} />
ui/src/components/search/sigil/SigilInput.svelte+13−12
@@ -4,12 +4,12 @@
44 import ClanSelect from './ClanSelect.svelte';
55
66 import type { PartType } from 'types/sigil';
7− import { SigilQuery } from 'lib/sigil';
7+ import type { SigilQuery } from 'lib/sigil';
88
99 export let sigilQuery: SigilQuery;
1010 // export let clan: ('galaxy' | 'star' | 'planet') = 'planet';
1111 let focusedSymbolIndex: number = 0;
12− let inputMode: PartType = undefined;
12+ let inputComponents: string[] = [];
1313
1414 // let symbols: SymbolQuery = [];
1515
@@ -22,11 +22,6 @@
2222 focusedSymbolIndex = numActiveSymbols - 1;
2323 }
2424 }
25− $: {
26− if (!focusedSymbol.geon) {
27− inputMode = 'geon';
28− }
29− }
3025 // function updateSigilQuery() {
3126 // sigilQuery.setSymbol(focusedSymbol, focusedSymbolIndex);
3227 // }
@@ -42,15 +37,21 @@
4237 <SymbolInput
4338 bind:symbolQuery={symbol}
4439 focused={index === focusedSymbolIndex}
40+ {inputComponents}
4541 on:click={() => focusedSymbolIndex = index}
4642 />
4743 {/each}
4844 </div>
4945 </div>
50− <div class="bg-green-100 grow rounded-b-2xl xs:rounded-bl-none">
51− {inputMode}
52− <SymbolInputModeSelector bind:symbolQuery={focusedSymbol} bind:inputMode />
53− </div>
46+ {#if focusedSymbol}
47+ <div class="bg-green-100 grow rounded-b-2xl xs:rounded-bl-none">
48+ <SymbolInputModeSelector
49+ bind:symbolQuery={focusedSymbol}
50+ bind:inputComponents
51+ unfocusSymbol={() => focusedSymbolIndex = undefined}
52+ />
53+ </div>
54+ {/if}
5455 </div>
55− {sigilQuery.string}
56+ <!-- {sigilQuery.string} -->
5657</div>
ui/src/components/search/sigil/Symbol.svelte+35−2
@@ -1,15 +1,22 @@
11<script lang="ts">
2+ import { createEventDispatcher } from 'svelte';
23 import { sigilFromSymbols, stringRenderer as sr, symbolFromDef } from '@johnhyde/sigil-js';
34 import { sigilScalingFunction } from 'lib/sigil';
5+ import symbolPartClick from 'actions/symbolPartClick';
46
57 export let components: string[];
68 export let size: number;
79 export let fgColor: string = 'black';
810 export let bgColor: string = 'white';
911 export let altBgColor: string = undefined;
12+ export let hoverColor: string = 'blue';
1013 export let inverted: boolean = false;
14+ export let interactive: boolean = false;
15+ export let strokeWidthFactor: number = 1;
1116 let svgString: any;
1217
18+ const dispatch = createEventDispatcher();
19+
1320 $: {
1421 const symbol = symbolFromDef(components);
1522 let colors = [bgColor, fgColor];
@@ -25,7 +32,9 @@
2532 colors,
2633 margin: false,
2734 autoScaleStrokes: true,
28− strokeScalingFunctionV2: sigilScalingFunction,
35+ strokeScalingFunctionV2: (s) => {
36+ return strokeWidthFactor * sigilScalingFunction(s);
37+ },
2938 },
3039 );
3140 svgAST.attributes.preserveAspectRatio = 'xMidYMin slice';
@@ -34,8 +43,32 @@
3443 svgString = svgString.replace(/rect fill="[^"]+"/, `rect fill="${tempAltBgColor}"`);
3544 }
3645 }
46+
47+ function onPartClick(partId: string) {
48+ dispatch('partClick', partId);
49+ }
3750</script>
3851
39−<div class="aspect-square" style:max-width="{size}px" on:click>
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+
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+>
4073 {@html svgString}
4174</div>
ui/src/components/search/sigil/SymbolInput.svelte+40−4
@@ -1,20 +1,56 @@
11<script lang="ts">
22 import Symbol from './Symbol.svelte';
3+ import without from 'lodash/without';
34
4− import type { SymbolQuery } from 'types/sigil';
5+ import type { PartType, SymbolQuery } from 'types/sigil';
56
67 export let symbolQuery: SymbolQuery;
8+ // export let inputMode: PartType;
79 export let focused: boolean = false;
10+ export let inputComponents: string[] = [];
811 let size = 128;
912
1013 $: 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+ }
1131</script>
1232
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>
1434 {#if symbolQuery.components.length > 0}
15− <Symbol components={symbolQuery.components} {size} inverted={!symbolQuery.geon} />
16− {symbolQuery.geon}
35+ <Symbol components={symbolQuery.components} {size}
36+ inverted={!symbolQuery.geon}
37+ interactive={focused}
38+ hoverColor="red"
39+ on:partClick={onPartDelete}
40+ />
1741 {:else}
1842 <div class="m-1 rounded-sm border-4 border-gray-500 grow"></div>
1943 {/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}
2056</div>
ui/src/components/search/sigil/SymbolInputModeSelector.svelte+201−7
@@ -5,21 +5,215 @@
55 import Symbol from './Symbol.svelte';
66
77 import type { PartType, SymbolQuery } from 'types/sigil';
8− import { PART_TYPES } from 'types/sigil';
98
109 export let symbolQuery: SymbolQuery;
11− export let inputMode: PartType;
10+ // export let inputMode: PartType;
11+ export let inputComponents: string[] = [];
12+ export let unfocusSymbol: Function;
1213
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;
13130 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+ }
14191</script>
15192
16193<div class="grid grid-rows-2 grid-flow-col xs:grid-cols-2 xs:grid-flow-row place-content-center justify-center">
17− {#if inputMode === 'geon'}
18− <GeonSelector bind:symbolQuery bind:inputMode />
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>
206+ {#if modes[0] === 'geon'}
207+ <GeonSelector bind:symbolQuery {popMode} />
19208 {:else}
20− {#each PART_TYPES as partType}
21− <SymbolInputModeSelectorButton {partType} focused={inputMode === partType}
22− on:click={() => inputMode = partType}
209+ <!-- {#each PART_TYPES as partType}
210+ <SymbolInputModeSelectorButton {partType} focused={modes[0] === 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)}
23217 />
24218 {/each}
25219 {/if}
ui/src/components/search/sigil/SymbolInputModeSelectorButton.svelte+6−3
@@ -4,15 +4,18 @@
44 import type { PartType, SymbolQuery } from 'types/sigil';
55 import { PART_TYPES } from 'types/sigil';
66
7− export let partType: PartType;
7+ // export let partType: PartType;
8+ export let components: string[] = [];
89 export let focused: boolean = false;
910
1011 let size = 48;
1112 $: focusedClasses = focused ? 'border border-gold-s1 border-2' : '';
13+ $: includesGeon = components.find(c => c[0] === 'g') === undefined;
1214</script>
1315
1416<div class="border-2 w-[48px] {focusedClasses}" on:click>
15− {#if partType === 'geon'}
17+ <Symbol {components} {size} inverted={includesGeon} />
18+ <!-- {#if partType === 'geon'}
1619 <Symbol components={['ghb']} {size} />
1720 {:else if partType === 'line'}
1821 <Symbol components={['lvf', 'lvfl', 'lvfr', 'lhf', 'lhft', 'lhfb', 'lff', 'lbf']} {size} inverted />
@@ -33,6 +36,6 @@
3336 <Symbol components={['b1l1', 'b1l2', 'b1l3']} {size} inverted />
3437 {:else}
3538 not {partType}
36− {/if}
39+ {/if} -->
3740</div>
3841
ui/src/lib/sigil.ts+1−1
@@ -25,7 +25,7 @@ export class SigilQuery {
2525 // symbol.components = ['gci', 'abr622']
2626 } else {
2727 symbol.symbolType = 'suffix';
28− symbol.components = ['gsq', 'abl202']
28+ symbol.components = ['gsq', 'ablf2']
2929 }
3030 });
3131 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+
14export const CLANS = ['galaxy', 'star', 'planet'] as const;
25export type Clan = typeof CLANS[number];
36
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;
58export type PartType = typeof PART_TYPES[number];
69
710export const GEONS = ['gci', 'gsq', 'ghl', 'ghr', 'ght', 'ghb', 'gq1', 'gq2', 'gq3', 'gq4', 'gfs', 'gbs'];
@@ -9,6 +12,23 @@ export type Geon = typeof GEONS[number];
912
1013export type SymbolType = ('prefix' | 'suffix');
1114
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+
1232export class SymbolQuery {
1333 symbolType: SymbolType;
1434 components: string[];
@@ -16,6 +36,7 @@ export class SymbolQuery {
1636 constructor(symbolType: SymbolType = 'suffix', components = []) {
1737 this.symbolType = symbolType;
1838 this.components = components;
39+ this.cleanupParts();
1940 }
2041
2142 set geon(geon: Geon) {
@@ -27,4 +48,49 @@ export class SymbolQuery {
2748 get geon(): Geon {
2849 return this.components.find(c => c[0] === 'g');
2950 }
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+ }
3096}